aboutsummaryrefslogtreecommitdiff
path: root/src/wallet.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-11-16 01:59:39 +0100
committerFlorian Dold <florian.dold@gmail.com>2016-11-16 02:00:31 +0100
commitbd65bb67e25a79b019d745b7262b2008ce2adb15 (patch)
tree89e1b032103a63737f1a703e6a943832ef261704 /src/wallet.ts
parentf91466595b651721690133f58ab37f977539e95b (diff)
downloadwallet-core-bd65bb67e25a79b019d745b7262b2008ce2adb15.tar.xz
incrementally verify denoms
The denominations are not stored in a separate object store.
Diffstat (limited to 'src/wallet.ts')
-rw-r--r--src/wallet.ts321
1 files changed, 205 insertions, 116 deletions
diff --git a/src/wallet.ts b/src/wallet.ts
index 4388c61c0..4c4899bc2 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -36,11 +36,11 @@ import {
PayCoinInfo,
PreCoinRecord,
RefreshSessionRecord,
- ReserveCreationInfo,
+ ReserveCreationInfo,
ReserveRecord,
WalletBalance,
WalletBalanceEntry,
- WireInfo,
+ WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
} from "./types";
import {
HttpRequestLibrary,
@@ -52,7 +52,7 @@ import {
Index,
JoinResult,
QueryRoot,
- Store,
+ Store, JoinLeftResult,
} from "./query";
import {Checkable} from "./checkable";
import {
@@ -69,7 +69,7 @@ import {CryptoApi} from "./cryptoApi";
export interface CoinWithDenom {
coin: CoinRecord;
- denom: Denomination;
+ denom: DenominationRecord;
}
@@ -215,10 +215,10 @@ function setTimeout(f: any, t: number) {
}
-function isWithdrawableDenom(d: Denomination) {
+function isWithdrawableDenom(d: DenominationRecord) {
const now_sec = (new Date).getTime() / 1000;
- const stamp_withdraw_sec = getTalerStampSec(d.stamp_expire_withdraw);
- const stamp_start_sec = getTalerStampSec(d.stamp_start);
+ const stamp_withdraw_sec = getTalerStampSec(d.stampExpireWithdraw);
+ const stamp_start_sec = getTalerStampSec(d.stampStart);
// Withdraw if still possible to withdraw within a minute
if ((stamp_withdraw_sec + 60 > now_sec) && (now_sec >= stamp_start_sec)) {
return true;
@@ -229,13 +229,14 @@ function isWithdrawableDenom(d: Denomination) {
export type CoinSelectionResult = {exchangeUrl: string, cds: CoinWithDenom[]}|undefined;
-export function selectCoins(cds: CoinWithDenom[], paymentAmount: AmountJson, depositFeeLimit: AmountJson): CoinWithDenom[]|undefined {
+export function selectCoins(cds: CoinWithDenom[], paymentAmount: AmountJson,
+ depositFeeLimit: AmountJson): CoinWithDenom[]|undefined {
if (cds.length == 0) {
return undefined;
}
// Sort by ascending deposit fee
- cds.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit,
- o2.denom.fee_deposit));
+ cds.sort((o1, o2) => Amounts.cmp(o1.denom.feeDeposit,
+ o2.denom.feeDeposit));
let currency = cds[0].denom.value.currency;
let cdsResult: CoinWithDenom[] = [];
let accFee: AmountJson = Amounts.getZero(currency);
@@ -244,15 +245,17 @@ export function selectCoins(cds: CoinWithDenom[], paymentAmount: AmountJson, dep
let coversAmount = false;
let coversAmountWithFee = false;
for (let i = 0; i < cds.length; i++) {
- let {coin,denom} = cds[i];
+ let {coin, denom} = cds[i];
cdsResult.push(cds[i]);
- if (Amounts.cmp(denom.fee_deposit, coin.currentAmount) >= 0) {
+ if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) {
continue;
}
- accFee = Amounts.add(denom.fee_deposit, accFee).amount;
+ accFee = Amounts.add(denom.feeDeposit, accFee).amount;
accAmount = Amounts.add(coin.currentAmount, accAmount).amount;
coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0;
- coversAmountWithFee = Amounts.cmp(accAmount, Amounts.add(paymentAmount, denom.fee_deposit).amount) >= 0;
+ coversAmountWithFee = Amounts.cmp(accAmount,
+ Amounts.add(paymentAmount,
+ denom.feeDeposit).amount) >= 0;
isBelowFee = Amounts.cmp(accFee, depositFeeLimit) <= 0;
if ((coversAmount && isBelowFee) || coversAmountWithFee) {
return cdsResult;
@@ -268,9 +271,9 @@ export function selectCoins(cds: CoinWithDenom[], paymentAmount: AmountJson, dep
* amount, but never larger.
*/
function getWithdrawDenomList(amountAvailable: AmountJson,
- denoms: Denomination[]): Denomination[] {
+ denoms: DenominationRecord[]): DenominationRecord[] {
let remaining = Amounts.copy(amountAvailable);
- const ds: Denomination[] = [];
+ const ds: DenominationRecord[] = [];
denoms = denoms.filter(isWithdrawableDenom);
denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
@@ -281,7 +284,7 @@ function getWithdrawDenomList(amountAvailable: AmountJson,
for (let i = 0; i < 1000; i++) {
let found = false;
for (let d of denoms) {
- let cost = Amounts.add(d.value, d.fee_withdraw).amount;
+ let cost = Amounts.add(d.value, d.feeWithdraw).amount;
if (Amounts.cmp(remaining, cost) < 0) {
continue;
}
@@ -346,14 +349,26 @@ export namespace Stores {
]);
}
- export let exchanges: ExchangeStore = new ExchangeStore();
- export let transactions: TransactionsStore = new TransactionsStore();
- export let reserves: Store<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
- export let coins: CoinsStore = new CoinsStore();
- export let refresh: Store<RefreshSessionRecord> = new Store<RefreshSessionRecord>("refresh", {keyPath: "meltCoinPub"});
- export let history: HistoryStore = new HistoryStore();
- export let offers: OffersStore = new OffersStore();
- export let precoins: Store<PreCoinRecord> = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
+ class DenominationsStore extends Store<DenominationRecord> {
+ constructor() {
+ // case needed because of bug in type annotations
+ super("denominations",
+ {keyPath: ["denomPub", "exchangeBaseUrl"] as any as IDBKeyPath});
+ }
+
+ exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
+ denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub");
+ }
+
+ export const exchanges: ExchangeStore = new ExchangeStore();
+ export const transactions: TransactionsStore = new TransactionsStore();
+ export const reserves: Store<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
+ export const coins: CoinsStore = new CoinsStore();
+ export const refresh: Store<RefreshSessionRecord> = new Store<RefreshSessionRecord>("refresh", {keyPath: "meltCoinPub"});
+ export const history: HistoryStore = new HistoryStore();
+ export const offers: OffersStore = new OffersStore();
+ export const precoins: Store<PreCoinRecord> = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
+ export const denominations: DenominationsStore = new DenominationsStore();
}
@@ -467,13 +482,20 @@ export class Wallet {
console.error("db inconsistent");
continue;
}
- let coins: CoinRecord[] = await this.q().iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeHandle.url).toArray();
+ let coins: CoinRecord[] = await this.q()
+ .iterIndex(Stores.coins.exchangeBaseUrlIndex,
+ exchangeHandle.url)
+ .toArray();
if (!coins || coins.length == 0) {
continue;
}
// Denomination of the first coin, we assume that all other
// coins have the same currency
- let firstDenom = exchange.all_denoms.find((d) => d.denom_pub == coins[0].denomPub);
+ let firstDenom = await this.q().get(Stores.denominations,
+ [
+ exchangeHandle.url,
+ coins[0].denomPub
+ ]);
if (!firstDenom) {
throw Error("db inconsistent");
}
@@ -481,7 +503,8 @@ export class Wallet {
let cds: CoinWithDenom[] = [];
for (let i = 0; i < coins.length; i++) {
let coin = coins[i];
- let denom = exchange.all_denoms.find((d) => d.denom_pub == coin.denomPub);
+ let denom = await this.q().get(Stores.denominations,
+ [exchangeHandle.url, coin.denomPub]);
if (!denom) {
throw Error("db inconsistent");
}
@@ -641,7 +664,8 @@ export class Wallet {
* with the given hash.
*/
async executePayment(H_contract: string): Promise<any> {
- let t = await this.q().get<TransactionRecord>(Stores.transactions, H_contract);
+ let t = await this.q().get<TransactionRecord>(Stores.transactions,
+ H_contract);
if (!t) {
return {
success: false,
@@ -703,13 +727,14 @@ export class Wallet {
private async processPreCoin(preCoin: PreCoinRecord,
retryDelayMs = 100): Promise<void> {
- let exchange = await this.q().get(Stores.exchanges,
- preCoin.exchangeBaseUrl);
+ const exchange = await this.q().get(Stores.exchanges,
+ preCoin.exchangeBaseUrl);
if (!exchange) {
console.error("db inconsistend: exchange for precoin not found");
return;
}
- let denom = exchange.all_denoms.find((d) => d.denom_pub == preCoin.denomPub);
+ const denom = await this.q().get(Stores.denominations,
+ [exchange.baseUrl, preCoin.denomPub]);
if (!denom) {
console.error("db inconsistent: denom for precoin not found");
return;
@@ -725,7 +750,7 @@ export class Wallet {
let x = Amounts.sub(r.precoin_amount,
preCoin.coinValue,
- denom!.fee_withdraw);
+ denom.feeWithdraw);
if (x.saturated) {
console.error("database inconsistent");
throw AbortTransaction;
@@ -734,7 +759,7 @@ export class Wallet {
return r;
};
- let historyEntry: HistoryRecord = {
+ const historyEntry: HistoryRecord = {
type: "withdraw",
timestamp: (new Date).getTime(),
level: HistoryLevel.Expert,
@@ -897,9 +922,12 @@ export class Wallet {
if (!reserve.current_amount) {
throw Error("can't withdraw when amount is unknown");
}
- let denomsAvailable: Denomination[] = Array.from(exchange.active_denoms);
- let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount!,
- denomsAvailable);
+ let currentAmount = reserve.current_amount;
+ if (!currentAmount) {
+ throw Error("can't withdraw when amount is unknown");
+ }
+ let denomsForWithdraw = await this.getVerifiedWithdrawDenomList(exchange.baseUrl,
+ currentAmount);
let ps = denomsForWithdraw.map(async(denom) => {
function mutateReserve(r: ReserveRecord): ReserveRecord {
@@ -909,10 +937,10 @@ export class Wallet {
}
r.precoin_amount = Amounts.add(r.precoin_amount,
denom.value,
- denom.fee_withdraw).amount;
+ denom.feeWithdraw).amount;
let result = Amounts.sub(currentAmount,
denom.value,
- denom.fee_withdraw);
+ denom.feeWithdraw);
if (result.saturated) {
console.error("can't create precoin, saturated");
throw AbortTransaction;
@@ -1000,19 +1028,81 @@ export class Wallet {
return wiJson;
}
+ async getPossibleDenoms(exchangeBaseUrl: string) {
+ return (
+ this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex,
+ exchangeBaseUrl)
+ .filter((d) => d.status == DenominationStatus.Unverified || d.status == DenominationStatus.VerifiedGood)
+ .toArray()
+ );
+ }
+
+ /**
+ * Get a list of denominations to withdraw from the given exchange for the
+ * given amount, making sure that all denominations' signatures are verified.
+ *
+ * Writes to the DB in order to record the result from verifying
+ * denominations.
+ */
+ async getVerifiedWithdrawDenomList(exchangeBaseUrl: string,
+ amount: AmountJson): Promise<DenominationRecord[]> {
+ const exchange = await this.q().get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ throw Error(`exchange ${exchangeBaseUrl} not found`);
+ }
+
+ const possibleDenoms = await (
+ this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex,
+ exchange.baseUrl)
+ .filter((d) => d.status == DenominationStatus.Unverified || d.status == DenominationStatus.VerifiedGood)
+ .toArray()
+ );
+
+ let allValid = false;
+ let currentPossibleDenoms = possibleDenoms;
+
+ let selectedDenoms: DenominationRecord[];
+
+ do {
+ allValid = true;
+ let nextPossibleDenoms = [];
+ selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
+ for (let denom of selectedDenoms || []) {
+ if (denom.status == DenominationStatus.Unverified) {
+ console.log(`verifying denom ${denom.denomPub.substr(0, 15)}`);
+ let valid = await this.cryptoApi.isValidDenom(denom,
+ exchange.masterPublicKey);
+ if (!valid) {
+ denom.status = DenominationStatus.VerifiedBad;
+ allValid = false;
+ } else {
+ denom.status = DenominationStatus.VerifiedGood;
+ nextPossibleDenoms.push(denom);
+ }
+ await this.q().put(Stores.denominations, denom).finish();
+ } else {
+ nextPossibleDenoms.push(denom);
+ }
+ }
+ currentPossibleDenoms = nextPossibleDenoms;
+ } while (selectedDenoms.length > 0 && !allValid);
+
+ return selectedDenoms;
+ }
+
async getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
- let selectedDenoms = getWithdrawDenomList(amount,
- exchangeInfo.active_denoms);
+ let selectedDenoms = await this.getVerifiedWithdrawDenomList(baseUrl,
+ amount);
let acc = Amounts.getZero(amount.currency);
for (let d of selectedDenoms) {
- acc = Amounts.add(acc, d.fee_withdraw).amount;
+ acc = Amounts.add(acc, d.feeWithdraw).amount;
}
let actualCoinCost = selectedDenoms
- .map((d: Denomination) => Amounts.add(d.value,
- d.fee_withdraw).amount)
+ .map((d: DenominationRecord) => Amounts.add(d.value,
+ d.feeWithdraw).amount)
.reduce((a, b) => Amounts.add(a, b).amount);
let wireInfo = await this.getWireInfo(baseUrl);
@@ -1044,13 +1134,17 @@ export class Wallet {
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson);
}
+
private async suspendCoins(exchangeInfo: ExchangeRecord): Promise<void> {
let suspendedCoins = await (
this.q()
.iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl)
- .reduce((coin: CoinRecord, suspendedCoins: CoinRecord[]) => {
- if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) {
- return Array.prototype.concat(suspendedCoins, [coin]);
+ .indexJoinLeft(Stores.denominations.exchangeBaseUrlIndex,
+ (e) => e.exchangeBaseUrl)
+ .reduce((cd: JoinLeftResult<CoinRecord,DenominationRecord>,
+ suspendedCoins: CoinRecord[]) => {
+ if (!cd.right || !cd.right.isOffered) {
+ return Array.prototype.concat(suspendedCoins, [cd.left]);
}
return Array.prototype.concat(suspendedCoins);
}, []));
@@ -1072,25 +1166,24 @@ export class Wallet {
throw Error("invalid update time");
}
- let r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
+ const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
let exchangeInfo: ExchangeRecord;
if (!r) {
exchangeInfo = {
baseUrl,
- all_denoms: [],
- active_denoms: [],
- last_update_time: updateTimeSec,
+ lastUpdateTime: updateTimeSec,
masterPublicKey: exchangeKeysJson.master_public_key,
};
console.log("making fresh exchange");
} else {
- if (updateTimeSec < r.last_update_time) {
+ if (updateTimeSec < r.lastUpdateTime) {
console.log("outdated /keys, not updating");
return r
}
exchangeInfo = r;
+ exchangeInfo.lastUpdateTime = updateTimeSec;
console.log("updating old exchange");
}
@@ -1112,54 +1205,34 @@ export class Wallet {
throw Error("public keys do not match");
}
- exchangeInfo.active_denoms = [];
-
- let denomsToCheck = newKeys.denoms.filter((newDenom) => {
- // did we find the new denom in the list of all (old) denoms?
- let found = false;
- for (let oldDenom of exchangeInfo.all_denoms) {
- if (oldDenom.denom_pub === newDenom.denom_pub) {
- let a: any = Object.assign({}, oldDenom);
- let b: any = Object.assign({}, newDenom);
- // pub hash is only there for convenience in the wallet
- delete a["pub_hash"];
- delete b["pub_hash"];
- if (!deepEquals(a, b)) {
- console.error("denomination parameters were modified, old/new:");
- console.dir(a);
- console.dir(b);
- // FIXME: report to auditors
- }
- found = true;
- break;
- }
- }
+ const existingDenoms: {[denomPub: string]: DenominationRecord} = await (
+ this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex,
+ exchangeInfo.baseUrl)
+ .reduce((x: DenominationRecord,
+ acc: typeof existingDenoms) => (acc[x.denomPub] = x, acc),
+ {})
+ );
- if (found) {
- exchangeInfo.active_denoms.push(newDenom);
- // No need to check signatures
- return false;
- }
- return true;
- });
+ const newDenoms: typeof existingDenoms = {};
- let ps = denomsToCheck.map(async(denom) => {
- let valid = await this.cryptoApi
- .isValidDenom(denom,
- exchangeInfo.masterPublicKey);
- if (!valid) {
- console.error("invalid denomination",
- denom,
- "with key",
- exchangeInfo.masterPublicKey);
- // FIXME: report to auditors
+ for (let d of newKeys.denoms) {
+ if (!(d.denom_pub in existingDenoms)) {
+ let dr = denominationRecordFromKeys(exchangeInfo.baseUrl, d);
+ newDenoms[dr.denomPub] = dr;
}
- exchangeInfo.active_denoms.push(denom);
- exchangeInfo.all_denoms.push(denom);
- });
+ }
- await Promise.all(ps);
+ for (let oldDenomPub in existingDenoms) {
+ if (!(oldDenomPub in newDenoms)) {
+ let d = existingDenoms[oldDenomPub];
+ d.isOffered = false;
+ }
+ }
+ await this.q()
+ .putAll(Stores.denominations,
+ Object.keys(newDenoms).map((d) => newDenoms[d]))
+ .finish();
return exchangeInfo;
}
@@ -1209,7 +1282,8 @@ export class Wallet {
return balance;
}
- function collectPendingRefresh(r: RefreshSessionRecord, balance: WalletBalance) {
+ function collectPendingRefresh(r: RefreshSessionRecord,
+ balance: WalletBalance) {
if (!r.finished) {
return balance;
}
@@ -1231,19 +1305,16 @@ export class Wallet {
return balance;
}
- function collectSmallestWithdraw(e: ExchangeRecord, sw: any) {
- let min: AmountJson|undefined;
- for (let d of e.active_denoms) {
- let v = Amounts.add(d.value, d.fee_withdraw).amount;
- if (!min) {
- min = v;
- continue;
- }
- if (Amounts.cmp(v, min) < 0) {
- min = v;
- }
+ function collectSmallestWithdraw(e: JoinResult<ExchangeRecord, DenominationRecord>,
+ sw: any) {
+ let min = sw[e.left.baseUrl];
+ let v = Amounts.add(e.right.value, e.right.feeWithdraw).amount;
+ if (!min) {
+ min = v;
+ } else if (Amounts.cmp(v, min) < 0) {
+ min = v;
}
- sw[e.baseUrl] = min;
+ sw[e.left.baseUrl] = min;
return sw;
}
@@ -1254,6 +1325,8 @@ export class Wallet {
smallestWithdraw = await (this.q()
.iter(Stores.exchanges)
+ .indexJoin(Stores.denominations.exchangeBaseUrlIndex,
+ (x) => x.baseUrl)
.reduce(collectSmallestWithdraw, {}));
console.log("smallest withdraws", smallestWithdraw);
@@ -1286,16 +1359,22 @@ export class Wallet {
throw Error("db inconsistent");
}
- let oldDenom = exchange.all_denoms.find((d) => d.denom_pub == coin!.denomPub);
+ let oldDenom = await this.q().get(Stores.denominations,
+ [exchange.baseUrl, coin.denomPub]);
if (!oldDenom) {
throw Error("db inconsistent");
}
- let availableDenoms: Denomination[] = exchange.active_denoms;
+ let availableDenoms: DenominationRecord[] = await (
+ this.q()
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex,
+ exchange.baseUrl)
+ .toArray()
+ );
let availableAmount = Amounts.sub(coin.currentAmount,
- oldDenom.fee_refresh).amount;
+ oldDenom.feeRefresh).amount;
let newCoinDenoms = getWithdrawDenomList(availableAmount,
availableDenoms);
@@ -1313,7 +1392,7 @@ export class Wallet {
3,
coin,
newCoinDenoms,
- oldDenom.fee_refresh));
+ oldDenom.feeRefresh));
function mutateCoin(c: CoinRecord): CoinRecord {
let r = Amounts.sub(c.currentAmount,
@@ -1467,7 +1546,13 @@ export class Wallet {
let coins: CoinRecord[] = [];
for (let i = 0; i < respJson.ev_sigs.length; i++) {
- let denom = exchange.all_denoms.find((d) => d.denom_pub == refreshSession.newDenoms[i]);
+ let denom = await (
+ this.q()
+ .get(Stores.denominations,
+ [
+ refreshSession.exchangeBaseUrl,
+ refreshSession.newDenoms[i]
+ ]));
if (!denom) {
console.error("denom not found");
continue;
@@ -1475,11 +1560,11 @@ export class Wallet {
let pc = refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i];
let denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig,
pc.blindingKey,
- denom.denom_pub);
+ denom.denomPub);
let coin: CoinRecord = {
coinPub: pc.publicKey,
coinPriv: pc.privateKey,
- denomPub: denom.denom_pub,
+ denomPub: denom.denomPub,
denomSig: denomSig,
currentAmount: denom.value,
exchangeBaseUrl: refreshSession.exchangeBaseUrl,
@@ -1502,7 +1587,7 @@ export class Wallet {
/**
* Retrive the full event history for this wallet.
*/
- async getHistory(): Promise<any> {
+ async getHistory(): Promise<{history: HistoryRecord[]}> {
function collect(x: any, acc: any) {
acc.push(x);
return acc;
@@ -1516,6 +1601,10 @@ export class Wallet {
return {history};
}
+ async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
+ let denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl).toArray();
+ return denoms;
+ }
async getOffer(offerId: number): Promise<any> {
let offer = await this.q() .get(Stores.offers, offerId);