prevent conflicting coin allocation with concurrent payments

This commit is contained in:
Florian Dold 2021-06-22 18:43:11 +02:00
parent 39c4b42daf
commit 09d1dd83ec
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 44 additions and 14 deletions

View File

@ -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;

View File

@ -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);
});

View File

@ -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}`);
});
}