commit 6affddc50527a8a4176e334166bff6b63c646ca6
parent 054af1c13e7f09ebb25437e9a38b36db7c0cfc5e
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:45 +0200
wallet-core: reject incomplete withdrawal batches
Diffstat:
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/packages/taler-wallet-core/src/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts
@@ -30,7 +30,10 @@ import {
} from "./db-common.js";
import { WalletDenomination } from "./db-common.js";
import { selectWithdrawalDenominations } from "./denomSelection.js";
-import { isWithdrawalFinalStatus } from "./withdraw.js";
+import {
+ isWithdrawalFinalStatus,
+ requireWithdrawalBatchCardinality,
+} from "./withdraw.js";
test("every terminal withdrawal status ends final-state waits", () => {
for (const status of [
@@ -54,6 +57,18 @@ test("every terminal withdrawal status ends final-state waits", () => {
);
});
+test("withdrawal batches require one signature per planchet", () => {
+ assert.doesNotThrow(() => requireWithdrawalBatchCardinality(2, 2));
+ assert.throws(
+ () => requireWithdrawalBatchCardinality(2, 1),
+ /returned 1 withdrawal signatures for 2 planchets/,
+ );
+ assert.throws(
+ () => requireWithdrawalBatchCardinality(2, 3),
+ /returned 3 withdrawal signatures for 2 planchets/,
+ );
+});
+
test("withdrawal selection bug repro", (t) => {
const amount = {
currency: "KUDOS",
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -1535,6 +1535,20 @@ interface WithdrawalBatchResult {
batchResp: ExchangeWithdrawResponse;
}
+export function requireWithdrawalBatchCardinality(
+ requestedCount: number,
+ responseCount: number,
+): void {
+ if (requestedCount === responseCount) {
+ return;
+ }
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_TRANSACTION_PROTOCOL_VIOLATION,
+ {},
+ `exchange returned ${responseCount} withdrawal signatures for ${requestedCount} planchets`,
+ );
+}
+
/**
* Transition a transaction from pending(ready)
* into a pending(kyc|aml) state, in case KYC is required.
@@ -1717,6 +1731,10 @@ async function processPlanchetExchangeLegacyBatchRequest(
resp,
codecForExchangeLegacyWithdrawBatchResponse(),
);
+ requireWithdrawalBatchCardinality(
+ requestCoinIdxs.length,
+ r.ev_sigs.length,
+ );
return {
coinIdxs: requestCoinIdxs,
batchResp: { ev_sigs: r.ev_sigs.map((x) => x.ev_sig) },
@@ -1910,6 +1928,10 @@ async function processPlanchetExchangeBatchRequest(
// Forbidden or any unexpected status: treated as an error below.
throwUnexpectedRequestError(resp.response, resp.detail!);
}
+ requireWithdrawalBatchCardinality(
+ requestCoinIdxs.length,
+ resp.body.ev_sigs.length,
+ );
return {
coinIdxs: requestCoinIdxs,
batchResp: resp.body,