diff options
author | Florian Dold <florian@dold.me> | 2022-09-14 20:34:37 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-09-14 20:40:38 +0200 |
commit | c021876b41bff11ad28c3a43808795fa0d02ce99 (patch) | |
tree | c92f4e83def462ddb0d446c9c476fd32f648d744 /packages/taler-wallet-core/src | |
parent | 9d044058e267e9e94f3ee63809a1e22426151ee5 (diff) | |
download | wallet-core-c021876b41bff11ad28c3a43808795fa0d02ce99.tar.xz |
wallet-core: cache fresh coin count in DB
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 17 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup/import.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 20 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 77 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/peer-to-peer.ts | 46 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refresh.ts | 23 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/tip.ts | 6 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/withdraw.ts | 7 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/query.ts | 5 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 123 |
10 files changed, 196 insertions, 129 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index da566ff24..6dfa63c06 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -314,6 +314,11 @@ export interface DenominationRecord { * that includes this denomination. */ listIssueDate: TalerProtocolTimestamp; + + /** + * Number of fresh coins of this denomination that are available. + */ + freshCoinCount?: number; } /** @@ -520,6 +525,13 @@ export enum CoinStatus { * Withdrawn and never shown to anybody. */ Fresh = "fresh", + + /** + * Fresh, but currently marked as "suspended", thus won't be used + * for spending. Used for testing. + */ + FreshSuspended = "fresh-suspended", + /** * A coin that has been spent and refreshed. */ @@ -606,11 +618,6 @@ export interface CoinRecord { exchangeBaseUrl: string; /** - * The coin is currently suspended, and will not be used for payments. - */ - suspended: boolean; - - /** * Blinding key used when withdrawing the coin. * Potentionally used again during payback. */ diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 8f5d019d4..53e45918e 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -413,7 +413,6 @@ export async function importBackup( currentAmount: Amounts.parseOrThrow(backupCoin.current_amount), denomSig: backupCoin.denom_sig, coinPub: compCoin.coinPub, - suspended: false, exchangeBaseUrl: backupExchangeDetails.base_url, denomPubHash, status: backupCoin.fresh diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 5838be765..6d63def59 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -33,12 +33,11 @@ import { getRandomBytes, hashWire, Logger, - NotificationType, parsePaytoUri, PayCoinSelection, PrepareDepositRequest, PrepareDepositResponse, - TalerErrorDetail, + RefreshReason, TalerProtocolTimestamp, TrackDepositGroupRequest, TrackDepositGroupResponse, @@ -46,18 +45,15 @@ import { } from "@gnu-taler/taler-util"; import { DepositGroupRecord, - OperationAttemptErrorResult, OperationAttemptResult, OperationStatus, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { selectPayCoins } from "../util/coinSelection.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { RetryInfo } from "../util/retries.js"; -import { guardOperationException } from "./common.js"; +import { spendCoins } from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { - applyCoinSpend, CoinSelectionRequest, extractContractData, generateDepositPermissions, @@ -525,12 +521,12 @@ export async function createDepositGroup( x.refreshGroups, ]) .runReadWrite(async (tx) => { - await applyCoinSpend( - ws, - tx, - payCoinSel, - `deposit-group:${depositGroup.depositGroupId}`, - ); + await spendCoins(ws, tx, { + allocationId: `deposit-group:${depositGroup.depositGroupId}`, + coinPubs: payCoinSel.coinPubs, + contributions: payCoinSel.coinContributions, + refreshReason: RefreshReason.PayDeposit, + }); await tx.depositGroups.put(depositGroup); }); diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 322e90487..bd7b1f7f0 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -100,6 +100,7 @@ import { } from "../util/http.js"; import { GetReadWriteAccess } from "../util/query.js"; import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js"; +import { spendCoins } from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; @@ -156,9 +157,6 @@ export async function getTotalPaymentCost( } function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean { - if (coin.suspended) { - return false; - } if (denom.isRevoked) { return false; } @@ -348,65 +346,6 @@ export async function getCandidatePayCoins( } /** - * Apply a coin selection to the database. Marks coins as spent - * and creates a refresh session for the remaining amount. - * - * FIXME: This does not deal well with conflicting spends! - * When two payments are made in parallel, the same coin can be selected - * for two payments. - * However, this is a situation that can also happen via sync. - */ -export async function applyCoinSpend( - ws: InternalWalletState, - tx: GetReadWriteAccess<{ - coins: typeof WalletStoresV1.coins; - refreshGroups: typeof WalletStoresV1.refreshGroups; - denominations: typeof WalletStoresV1.denominations; - }>, - coinSelection: PayCoinSelection, - allocationId: string, -): Promise<void> { - logger.info(`applying coin spend ${j2s(coinSelection)}`); - for (let i = 0; i < coinSelection.coinPubs.length; i++) { - const coin = await tx.coins.get(coinSelection.coinPubs[i]); - if (!coin) { - throw Error("coin allocated for payment doesn't exist anymore"); - } - const contrib = coinSelection.coinContributions[i]; - if (coin.status !== CoinStatus.Fresh) { - const alloc = coin.allocation; - if (!alloc) { - continue; - } - if (alloc.id !== allocationId) { - // FIXME: assign error code - throw Error("conflicting coin allocation (id)"); - } - if (0 !== Amounts.cmp(alloc.amount, contrib)) { - // FIXME: assign error code - throw Error("conflicting coin allocation (contrib)"); - } - continue; - } - coin.status = CoinStatus.Dormant; - coin.allocation = { - id: allocationId, - amount: Amounts.stringify(contrib), - }; - const remaining = Amounts.sub(coin.currentAmount, contrib); - if (remaining.saturated) { - throw Error("not enough remaining balance on coin for payment"); - } - coin.currentAmount = remaining.amount; - await tx.coins.put(coin); - } - const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({ - coinPub: x, - })); - await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant); -} - -/** * Record all information that is necessary to * pay for a proposal in the wallet's database. */ @@ -468,7 +407,12 @@ async function recordConfirmPay( await tx.proposals.put(p); } await tx.purchases.put(t); - await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`); + await spendCoins(ws, tx, { + allocationId: `proposal:${t.proposalId}`, + coinPubs: coinSelection.coinPubs, + contributions: coinSelection.coinContributions, + refreshReason: RefreshReason.PayMerchant, + }); }); ws.notify({ @@ -1038,7 +982,12 @@ async function handleInsufficientFunds( p.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); p.coinDepositPermissions = undefined; await tx.purchases.put(p); - await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`); + await spendCoins(ws, tx, { + allocationId: `proposal:${p.proposalId}`, + coinPubs: p.payCoinSelection.coinPubs, + contributions: p.payCoinSelection.coinContributions, + refreshReason: RefreshReason.PayMerchant, + }); }); } diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index 59dad3d55..449a91c68 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -75,6 +75,8 @@ import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { createRefreshGroup } from "./refresh.js"; import { updateExchangeFromUrl } from "./exchanges.js"; +import { spendCoins } from "../wallet.js"; +import { RetryTags } from "../util/retries.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -256,18 +258,14 @@ export async function initiatePeerToPeerPush( return undefined; } - const pubs: CoinPublicKey[] = []; - for (const c of sel.coins) { - const coin = await tx.coins.get(c.coinPub); - checkDbInvariant(!!coin); - coin.currentAmount = Amounts.sub( - coin.currentAmount, - Amounts.parseOrThrow(c.contribution), - ).amount; - coin.status = CoinStatus.Dormant; - pubs.push({ coinPub: coin.coinPub }); - await tx.coins.put(coin); - } + await spendCoins(ws, tx, { + allocationId: `peer-push:${pursePair.pub}`, + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPush, + }); await tx.peerPushPaymentInitiations.add({ amount: Amounts.stringify(instructedAmount), @@ -284,8 +282,6 @@ export async function initiatePeerToPeerPush( timestampCreated: TalerProtocolTimestamp.now(), }); - await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPush); - return sel; }); logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`); @@ -588,20 +584,14 @@ export async function acceptPeerPullPayment( return undefined; } - const pubs: CoinPublicKey[] = []; - for (const c of sel.coins) { - const coin = await tx.coins.get(c.coinPub); - checkDbInvariant(!!coin); - coin.currentAmount = Amounts.sub( - coin.currentAmount, - Amounts.parseOrThrow(c.contribution), - ).amount; - coin.status = CoinStatus.Dormant; - pubs.push({ coinPub: coin.coinPub }); - await tx.coins.put(coin); - } - - await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPull); + await spendCoins(ws, tx, { + allocationId: `peer-pull:${req.peerPullPaymentIncomingId}`, + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPull, + }); const pi = await tx.peerPullPaymentIncoming.get( req.peerPullPaymentIncomingId, diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 719093bd8..d1c366cd0 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -77,6 +77,7 @@ import { import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; import { RetryInfo, runOperationHandlerForResult } from "../util/retries.js"; +import { makeCoinAvailable } from "../wallet.js"; import { guardOperationException } from "./common.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { @@ -670,7 +671,6 @@ async function refreshReveal( type: CoinSourceType.Refresh, oldCoinPub: refreshGroup.oldCoinPubs[coinIndex], }, - suspended: false, coinEvHash: pc.coinEvHash, ageCommitmentProof: pc.ageCommitmentProof, }; @@ -680,7 +680,7 @@ async function refreshReveal( } await ws.db - .mktx((x) => [x.coins, x.refreshGroups]) + .mktx((x) => [x.coins, x.denominations, x.refreshGroups]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { @@ -694,7 +694,7 @@ async function refreshReveal( rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; updateGroupStatus(rg); for (const coin of coins) { - await tx.coins.put(coin); + await makeCoinAvailable(ws, tx, coin); } await tx.refreshGroups.put(rg); }); @@ -865,10 +865,22 @@ export async function createRefreshGroup( !!denom, "denomination for existing coin must be in database", ); + if (coin.status !== CoinStatus.Dormant) { + coin.status = CoinStatus.Dormant; + const denom = await tx.denominations.get([ + coin.exchangeBaseUrl, + coin.denomPubHash, + ]); + checkDbInvariant(!!denom); + checkDbInvariant( + denom.freshCoinCount != null && denom.freshCoinCount > 0, + ); + denom.freshCoinCount--; + await tx.denominations.put(denom); + } const refreshAmount = coin.currentAmount; inputPerCoin.push(refreshAmount); coin.currentAmount = Amounts.getZero(refreshAmount.currency); - coin.status = CoinStatus.Dormant; await tx.coins.put(coin); const denoms = await getDenoms(coin.exchangeBaseUrl); const cost = getTotalRefreshCost(denoms, denom, refreshAmount); @@ -965,9 +977,6 @@ export async function autoRefresh( if (coin.status !== CoinStatus.Fresh) { continue; } - if (coin.suspended) { - continue; - } const denom = await tx.denominations.get([ exchangeBaseUrl, coin.denomPubHash, diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 04da2b988..f70e2d02b 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -51,6 +51,7 @@ import { readSuccessResponseJsonOrThrow, } from "../util/http.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; +import { makeCoinAvailable } from "../wallet.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { getCandidateWithdrawalDenoms, @@ -310,13 +311,12 @@ export async function processTip( denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig }, exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, - suspended: false, coinEvHash: planchet.coinEvHash, }); } await ws.db - .mktx((x) => [x.coins, x.tips, x.withdrawalGroups]) + .mktx((x) => [x.coins, x.denominations, x.tips]) .runReadWrite(async (tx) => { const tr = await tx.tips.get(walletTipId); if (!tr) { @@ -328,7 +328,7 @@ export async function processTip( tr.pickedUpTimestamp = TalerProtocolTimestamp.now(); await tx.tips.put(tr); for (const cr of newCoinRecords) { - await tx.coins.put(cr); + await makeCoinAvailable(ws, tx, cr); } }); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 1b8383776..bee83265c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -93,11 +93,11 @@ import { } from "../util/http.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; -import { RetryInfo } from "../util/retries.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, } from "../versions.js"; +import { makeCoinAvailable } from "../wallet.js"; import { getExchangeDetails, getExchangePaytoUri, @@ -805,7 +805,6 @@ async function processPlanchetVerifyAndStoreCoin( reservePub: planchet.reservePub, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, }, - suspended: false, ageCommitmentProof: planchet.ageCommitmentProof, }; @@ -815,7 +814,7 @@ async function processPlanchetVerifyAndStoreCoin( // withdrawal succeeded. If so, mark the withdrawal // group as finished. const firstSuccess = await ws.db - .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets]) + .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets]) .runReadWrite(async (tx) => { const p = await tx.planchets.get(planchetCoinPub); if (!p || p.withdrawalDone) { @@ -823,7 +822,7 @@ async function processPlanchetVerifyAndStoreCoin( } p.withdrawalDone = true; await tx.planchets.put(p); - await tx.coins.add(coin); + await makeCoinAvailable(ws, tx, coin); return true; }); diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 025959253..17b713659 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -445,14 +445,15 @@ function runTx<Arg, Res>( if (!gotFunResult) { const msg = "BUG: transaction closed before transaction function returned"; - console.error(msg); + logger.error(msg); + logger.error(`${stack.stack}`); reject(Error(msg)); } resolve(funResult); }; tx.onerror = () => { logger.error("error in transaction"); - logger.error(`${stack}`); + logger.error(`${stack.stack}`); }; tx.onabort = () => { let msg: string; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 0e7772259..afbee4e64 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -99,7 +99,9 @@ import { } from "./crypto/workers/cryptoDispatcher.js"; import { AuditorTrustRecord, + CoinRecord, CoinSourceType, + CoinStatus, exportDb, importDb, OperationAttemptResult, @@ -216,6 +218,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "./util/http.js"; +import { checkDbInvariant } from "./util/invariants.js"; import { AsyncCondition, OpenedPromise, @@ -787,21 +790,135 @@ async function getExchangeDetailedInfo( }; } +export async function makeCoinAvailable( + ws: InternalWalletState, + tx: GetReadWriteAccess<{ + coins: typeof WalletStoresV1.coins; + denominations: typeof WalletStoresV1.denominations; + }>, + coinRecord: CoinRecord, +): Promise<void> { + const denom = await tx.denominations.get([ + coinRecord.exchangeBaseUrl, + coinRecord.denomPubHash, + ]); + checkDbInvariant(!!denom); + if (!denom.freshCoinCount) { + denom.freshCoinCount = 0; + } + denom.freshCoinCount++; + await tx.coins.put(coinRecord); + await tx.denominations.put(denom); +} + +export interface CoinsSpendInfo { + coinPubs: string[]; + contributions: AmountJson[]; + refreshReason: RefreshReason; + /** + * Identifier for what the coin has been spent for. + */ + allocationId: string; +} + +export async function spendCoins( + ws: InternalWalletState, + tx: GetReadWriteAccess<{ + coins: typeof WalletStoresV1.coins; + refreshGroups: typeof WalletStoresV1.refreshGroups; + denominations: typeof WalletStoresV1.denominations; + }>, + csi: CoinsSpendInfo, +): Promise<void> { + for (let i = 0; i < csi.coinPubs.length; i++) { + const coin = await tx.coins.get(csi.coinPubs[i]); + if (!coin) { + throw Error("coin allocated for payment doesn't exist anymore"); + } + const denom = await tx.denominations.get([ + coin.exchangeBaseUrl, + coin.denomPubHash, + ]); + checkDbInvariant(!!denom); + const contrib = csi.contributions[i]; + if (coin.status !== CoinStatus.Fresh) { + const alloc = coin.allocation; + if (!alloc) { + continue; + } + if (alloc.id !== csi.allocationId) { + // FIXME: assign error code + throw Error("conflicting coin allocation (id)"); + } + if (0 !== Amounts.cmp(alloc.amount, contrib)) { + // FIXME: assign error code + throw Error("conflicting coin allocation (contrib)"); + } + continue; + } + coin.status = CoinStatus.Dormant; + coin.allocation = { + id: csi.allocationId, + amount: Amounts.stringify(contrib), + }; + const remaining = Amounts.sub(coin.currentAmount, contrib); + if (remaining.saturated) { + throw Error("not enough remaining balance on coin for payment"); + } + coin.currentAmount = remaining.amount; + checkDbInvariant(!!denom); + if (denom.freshCoinCount == null || denom.freshCoinCount === 0) { + throw Error(`invalid coin count ${denom.freshCoinCount} in DB`); + } + denom.freshCoinCount--; + await tx.coins.put(coin); + await tx.denominations.put(denom); + } + const refreshCoinPubs = csi.coinPubs.map((x) => ({ + coinPub: x, + })); + await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant); +} + async function setCoinSuspended( ws: InternalWalletState, coinPub: string, suspended: boolean, ): Promise<void> { await ws.db - .mktx((x) => [x.coins]) + .mktx((x) => [x.coins, x.denominations]) .runReadWrite(async (tx) => { const c = await tx.coins.get(coinPub); if (!c) { logger.warn(`coin ${coinPub} not found, won't suspend`); return; } - c.suspended = suspended; + const denom = await tx.denominations.get([ + c.exchangeBaseUrl, + c.denomPubHash, + ]); + checkDbInvariant(!!denom); + if (suspended) { + if (c.status !== CoinStatus.Fresh) { + return; + } + if (denom.freshCoinCount == null || denom.freshCoinCount === 0) { + throw Error(`invalid coin count ${denom.freshCoinCount} in DB`); + } + denom.freshCoinCount--; + c.status = CoinStatus.FreshSuspended; + } else { + if (c.status == CoinStatus.Dormant) { + return; + } + if (denom.freshCoinCount == null) { + denom.freshCoinCount = 0; + } + denom.freshCoinCount++; + c.status = CoinStatus.Fresh; + } await tx.coins.put(c); + await tx.denominations.put(denom); }); } @@ -857,7 +974,7 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { refresh_parent_coin_pub: refreshParentCoinPub, remaining_value: Amounts.stringify(c.currentAmount), withdrawal_reserve_pub: withdrawalReservePub, - coin_suspended: c.suspended, + coin_suspended: c.status === CoinStatus.FreshSuspended, ageCommitmentProof: c.ageCommitmentProof, }); } |