prevent conflicting coin allocation with concurrent payments
This commit is contained in:
parent
39c4b42daf
commit
09d1dd83ec
@ -848,6 +848,17 @@ export interface CoinRecord {
|
|||||||
* Status of the coin.
|
* Status of the coin.
|
||||||
*/
|
*/
|
||||||
status: CoinStatus;
|
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 {
|
export enum ProposalStatus {
|
||||||
@ -1643,6 +1654,8 @@ export interface DepositGroupRecord {
|
|||||||
|
|
||||||
payCoinSelection: PayCoinSelection;
|
payCoinSelection: PayCoinSelection;
|
||||||
|
|
||||||
|
payCoinSelectionUid: string;
|
||||||
|
|
||||||
totalPayCost: AmountJson;
|
totalPayCost: AmountJson;
|
||||||
|
|
||||||
effectiveDepositAmount: AmountJson;
|
effectiveDepositAmount: AmountJson;
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
timestampTruncateToSecond,
|
timestampTruncateToSecond,
|
||||||
TrackDepositGroupRequest,
|
TrackDepositGroupRequest,
|
||||||
TrackDepositGroupResponse,
|
TrackDepositGroupResponse,
|
||||||
URL
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { InternalWalletState } from "../common.js";
|
import { InternalWalletState } from "../common.js";
|
||||||
import { kdf } from "../crypto/primitives/kdf.js";
|
import { kdf } from "../crypto/primitives/kdf.js";
|
||||||
@ -433,7 +433,8 @@ export async function createDepositGroup(
|
|||||||
timestampCreated: timestamp,
|
timestampCreated: timestamp,
|
||||||
timestampFinished: undefined,
|
timestampFinished: undefined,
|
||||||
payCoinSelection: payCoinSel,
|
payCoinSelection: payCoinSel,
|
||||||
depositedPerCoin: payCoinSel.coinPubs.map((x) => false),
|
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
||||||
|
depositedPerCoin: payCoinSel.coinPubs.map(() => false),
|
||||||
merchantPriv: merchantPair.priv,
|
merchantPriv: merchantPair.priv,
|
||||||
merchantPub: merchantPair.pub,
|
merchantPub: merchantPair.pub,
|
||||||
totalPayCost: totalDepositCost,
|
totalPayCost: totalDepositCost,
|
||||||
@ -454,7 +455,12 @@ export async function createDepositGroup(
|
|||||||
denominations: x.denominations,
|
denominations: x.denominations,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
await applyCoinSpend(ws, tx, payCoinSel);
|
await applyCoinSpend(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
|
payCoinSel,
|
||||||
|
`deposit-group:${depositGroup.depositGroupId}`,
|
||||||
|
);
|
||||||
await tx.depositGroups.put(depositGroup);
|
await tx.depositGroups.put(depositGroup);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -385,24 +385,34 @@ export async function applyCoinSpend(
|
|||||||
denominations: typeof WalletStoresV1.denominations;
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
}>,
|
}>,
|
||||||
coinSelection: PayCoinSelection,
|
coinSelection: PayCoinSelection,
|
||||||
|
allocationId: string,
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < coinSelection.coinPubs.length; i++) {
|
for (let i = 0; i < coinSelection.coinPubs.length; i++) {
|
||||||
const coin = await tx.coins.get(coinSelection.coinPubs[i]);
|
const coin = await tx.coins.get(coinSelection.coinPubs[i]);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
throw Error("coin allocated for payment doesn't exist anymore");
|
throw Error("coin allocated for payment doesn't exist anymore");
|
||||||
}
|
}
|
||||||
|
const contrib = coinSelection.coinContributions[i];
|
||||||
if (coin.status !== CoinStatus.Fresh) {
|
if (coin.status !== CoinStatus.Fresh) {
|
||||||
// applyCoinSpend was called again, probably
|
const alloc = coin.allocation;
|
||||||
// because of a coin re-selection to recover after
|
if (!alloc) {
|
||||||
// accidental double spending.
|
continue;
|
||||||
// Ignore coins we already marked as spent.
|
}
|
||||||
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;
|
coin.status = CoinStatus.Dormant;
|
||||||
const remaining = Amounts.sub(
|
coin.allocation = {
|
||||||
coin.currentAmount,
|
id: allocationId,
|
||||||
coinSelection.coinContributions[i],
|
amount: Amounts.stringify(contrib),
|
||||||
);
|
};
|
||||||
|
const remaining = Amounts.sub(coin.currentAmount, contrib);
|
||||||
if (remaining.saturated) {
|
if (remaining.saturated) {
|
||||||
throw Error("not enough remaining balance on coin for payment");
|
throw Error("not enough remaining balance on coin for payment");
|
||||||
}
|
}
|
||||||
@ -482,7 +492,7 @@ async function recordConfirmPay(
|
|||||||
await tx.proposals.put(p);
|
await tx.proposals.put(p);
|
||||||
}
|
}
|
||||||
await tx.purchases.put(t);
|
await tx.purchases.put(t);
|
||||||
await applyCoinSpend(ws, tx, coinSelection);
|
await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
@ -1082,9 +1092,10 @@ async function handleInsufficientFunds(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
p.payCoinSelection = res;
|
p.payCoinSelection = res;
|
||||||
|
p.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
|
||||||
p.coinDepositPermissions = undefined;
|
p.coinDepositPermissions = undefined;
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
await applyCoinSpend(ws, tx, res);
|
await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user