diff options
author | Florian Dold <florian@dold.me> | 2021-06-22 18:43:11 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-06-22 18:43:11 +0200 |
commit | 09d1dd83ec1bf9ca16841d0afb18b9a7da705bcb (patch) | |
tree | af089d994c233888f3f7291fdc148a44142296ed | |
parent | 39c4b42dafc5d8fc5f455e7ed936c45ec2340cfc (diff) |
prevent conflicting coin allocation with concurrent payments
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 13 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/deposits.ts | 12 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 33 |
3 files changed, 44 insertions, 14 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 2d2c0615c..36b4e0864 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -848,6 +848,17 @@ export interface CoinRecord { * Status of the coin. */ status: CoinStatus; + + /** + * Information about what the coin has been allocated for. + * Used to prevent allocation of the same coin for two different payments. + */ + allocation?: CoinAllocation; +} + +export interface CoinAllocation { + id: string; + amount: AmountString; } export enum ProposalStatus { @@ -1643,6 +1654,8 @@ export interface DepositGroupRecord { payCoinSelection: PayCoinSelection; + payCoinSelectionUid: string; + totalPayCost: AmountJson; effectiveDepositAmount: AmountJson; diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 9dee7557c..c788a9ea2 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -36,7 +36,7 @@ import { timestampTruncateToSecond, TrackDepositGroupRequest, TrackDepositGroupResponse, - URL + URL, } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../common.js"; import { kdf } from "../crypto/primitives/kdf.js"; @@ -433,7 +433,8 @@ export async function createDepositGroup( timestampCreated: timestamp, timestampFinished: undefined, payCoinSelection: payCoinSel, - depositedPerCoin: payCoinSel.coinPubs.map((x) => false), + payCoinSelectionUid: encodeCrock(getRandomBytes(32)), + depositedPerCoin: payCoinSel.coinPubs.map(() => false), merchantPriv: merchantPair.priv, merchantPub: merchantPair.pub, totalPayCost: totalDepositCost, @@ -454,7 +455,12 @@ export async function createDepositGroup( denominations: x.denominations, })) .runReadWrite(async (tx) => { - await applyCoinSpend(ws, tx, payCoinSel); + await applyCoinSpend( + ws, + tx, + payCoinSel, + `deposit-group:${depositGroup.depositGroupId}`, + ); 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 71f11c960..280586c34 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -385,24 +385,34 @@ export async function applyCoinSpend( denominations: typeof WalletStoresV1.denominations; }>, coinSelection: PayCoinSelection, + allocationId: string, ) { 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) { - // applyCoinSpend was called again, probably - // because of a coin re-selection to recover after - // accidental double spending. - // Ignore coins we already marked as spent. - continue; + 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)"); + } } coin.status = CoinStatus.Dormant; - const remaining = Amounts.sub( - coin.currentAmount, - coinSelection.coinContributions[i], - ); + 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"); } @@ -482,7 +492,7 @@ async function recordConfirmPay( await tx.proposals.put(p); } await tx.purchases.put(t); - await applyCoinSpend(ws, tx, coinSelection); + await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`); }); ws.notify({ @@ -1082,9 +1092,10 @@ async function handleInsufficientFunds( return; } p.payCoinSelection = res; + p.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); p.coinDepositPermissions = undefined; await tx.purchases.put(p); - await applyCoinSpend(ws, tx, res); + await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`); }); } |