taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 3529b94fe7c59ac357b8b8e053b3b0c70be42d15
parent 9c0fc4565460544a4f377d62bc5e18182e3ed7ca
Author: Florian Dold <florian@dold.me>
Date:   Mon, 14 Jul 2025 22:34:54 +0200

harness: add separate test to reproduce exchange crash

Diffstat:
Apackages/taler-harness/src/integrationtests/test-deposit-too-large.ts | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-wallet-core/src/dbless.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
3 files changed, 228 insertions(+), 26 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-deposit-too-large.ts b/packages/taler-harness/src/integrationtests/test-deposit-too-large.ts @@ -0,0 +1,166 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + AmountString, + encodeCrock, + getRandomBytes, + j2s, + TalerError, +} from "@gnu-taler/taler-util"; +import { + CryptoDispatcher, + SynchronousCryptoWorkerFactoryPlain, +} from "@gnu-taler/taler-wallet-core"; +import { + checkReserve, + CoinInfo, + depositCoinBatch, + downloadExchangeInfo, + findDenomOrThrow, + topupReserveWithBank, + withdrawCoin, +} from "@gnu-taler/taler-wallet-core/dbless"; +import { CoinConfig } from "../harness/denomStructures.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; + +const coinCommon = { + cipher: "RSA" as const, + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + feeDeposit: "TESTKUDOS:0", + feeRefresh: "TESTKUDOS:0", + feeRefund: "TESTKUDOS:0", + feeWithdraw: "TESTKUDOS:0", + rsaKeySize: 1024, +}; + +const coinConfigList: CoinConfig[] = [ + { + ...coinCommon, + name: "n1", + value: "TESTKUDOS:1", + }, +]; + +/** + * Test deposit with a large number of coins. + * + * In particular, this checks that the wallet properly + * splits deposits into batches with <=64 coins per batch. + * + * Since we use an artifically large number of coins, this + * test is a bit slower than other tests. + */ +export async function runDepositTooLargeTest(t: GlobalTestState) { + // Set up test environment + + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + + const http = harnessHttpLib; + const cryptiDisp = new CryptoDispatcher( + new SynchronousCryptoWorkerFactoryPlain(), + ); + const cryptoApi = cryptiDisp.cryptoApi; + + const merchantPair = await cryptoApi.createEddsaKeypair({}); + const merchantPub = merchantPair.pub; + const merchantPriv = merchantPair.priv; + + try { + // Withdraw digital cash into the wallet. + + const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http); + + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + + let reserveUrl = new URL( + `reserves/${reserveKeyPair.pub}`, + exchange.baseUrl, + ); + reserveUrl.searchParams.set("timeout_ms", "30000"); + const longpollReq = http.fetch(reserveUrl.href, { + method: "GET", + }); + + await topupReserveWithBank({ + amount: "TESTKUDOS:20" as AmountString, + http, + reservePub: reserveKeyPair.pub, + corebankApiBaseUrl: bank.corebankApiBaseUrl, + exchangeInfo, + }); + + console.log("waiting for longpoll request"); + const resp = await longpollReq; + console.log(`got response, status ${resp.status}`); + + console.log(exchangeInfo); + + await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); + + const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:0.1" as AmountString); + + const coins: CoinInfo[] = []; + const amounts: AmountString[] = []; + + for (let i = 0; i < 100; i++) { + const coin = await withdrawCoin({ + http, + cryptoApi, + reserveKeyPair: { + reservePriv: reserveKeyPair.priv, + reservePub: reserveKeyPair.pub, + }, + denom: d1, + exchangeBaseUrl: exchange.baseUrl, + }); + coins.push(coin); + amounts.push("TESTKUDOS:0.1"); + } + + const wireSalt = encodeCrock(getRandomBytes(16)); + const contractTermsHash = encodeCrock(getRandomBytes(64)); + + await depositCoinBatch({ + contractTermsHash, + merchantPriv, + wireSalt, + amounts, + coins, + cryptoApi, + exchangeBaseUrl: exchange.baseUrl, + http, + }); + } catch (e) { + if (e instanceof TalerError) { + console.log(e); + console.log(j2s(e.errorDetail)); + } else { + console.log(e); + } + } + + { + // Try downloading exchange info again to make + // sure that exchange is still running and didn't crash! + const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http); + } +} + +runDepositTooLargeTest.suites = ["wallet", "slow"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -48,6 +48,7 @@ import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; import { runDepositFaultTest } from "./test-deposit-fault.js"; import { runDepositLargeTest } from "./test-deposit-large.js"; import { runDepositMergeTest } from "./test-deposit-merge.js"; +import { runDepositTooLargeTest } from "./test-deposit-too-large.js"; import { runDepositTest } from "./test-deposit.js"; import { runExchangeDepositTest } from "./test-exchange-deposit.js"; import { runExchangeKycAuthTest } from "./test-exchange-kyc-auth.js"; @@ -361,6 +362,7 @@ const allTests: TestMainFunction[] = [ runWalletExchangeMigrationTest, runKycFormCompressionTest, runDepositLargeTest, + runDepositTooLargeTest, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts @@ -31,6 +31,7 @@ import { AmountJson, AmountString, Amounts, + BatchDepositRequestCoin, DenominationPubKey, EddsaPrivateKeyString, EddsaPublicKeyString, @@ -215,7 +216,32 @@ export async function depositCoin(args: { // 16 bytes, crockford encoded wireSalt?: string; }): Promise<void> { - const { coin, http, cryptoApi } = args; + return await depositCoinBatch({ + coins: [args.coin], + amounts: [args.amount], + cryptoApi: args.cryptoApi, + exchangeBaseUrl: args.exchangeBaseUrl, + http: args.http, + merchantPriv: args.merchantPriv, + depositPayto: args.depositPayto, + contractTermsHash: args.contractTermsHash, + wireSalt: args.wireSalt, + }); +} + +export async function depositCoinBatch(args: { + http: HttpRequestLibrary; + cryptoApi: TalerCryptoInterface; + exchangeBaseUrl: string; + coins: CoinInfo[]; + amounts: AmountString[]; + depositPayto?: string; + merchantPriv: string; + contractTermsHash?: string; + // 16 bytes, crockford encoded + wireSalt?: string; +}): Promise<void> { + const { coins, amounts, http, cryptoApi } = args; const depositPayto = args.depositPayto ?? "payto://x-taler-bank/localhost/foo?receiver-name=foo"; const wireSalt = args.wireSalt ?? encodeCrock(getRandomBytes(16)); @@ -238,35 +264,43 @@ export async function depositCoin(args: { merchantPriv = res.priv; merchantPub = res.pub; } - const dp = await cryptoApi.signDepositPermission({ - coinPriv: coin.coinPriv, - coinPub: coin.coinPub, - contractTermsHash, - denomKeyType: coin.denomPub.cipher, - denomPubHash: coin.denomPubHash, - denomSig: coin.denomSig, - exchangeBaseUrl: args.exchangeBaseUrl, - feeDeposit: Amounts.parseOrThrow(coin.feeDeposit), - merchantPub, - spendAmount: Amounts.parseOrThrow(args.amount), - timestamp: depositTimestamp, - refundDeadline: refundDeadline, - wireInfoHash: hashWire(depositPayto, wireSalt), - }); + if (coins.length != amounts.length) { + throw Error(`coins and amounts must match`); + } + + const depositCoins: BatchDepositRequestCoin[] = []; + for (let i = 0; i < coins.length; i++) { + const coin = coins[i]; + const amount = amounts[i]; + const dp = await cryptoApi.signDepositPermission({ + coinPriv: coin.coinPriv, + coinPub: coin.coinPub, + contractTermsHash, + denomKeyType: coin.denomPub.cipher, + denomPubHash: coin.denomPubHash, + denomSig: coin.denomSig, + exchangeBaseUrl: args.exchangeBaseUrl, + feeDeposit: Amounts.parseOrThrow(coin.feeDeposit), + merchantPub, + spendAmount: Amounts.parseOrThrow(amount), + timestamp: depositTimestamp, + refundDeadline: refundDeadline, + wireInfoHash: hashWire(depositPayto, wireSalt), + }); + depositCoins.push({ + contribution: Amounts.stringify(dp.contribution), + coin_pub: dp.coin_pub, + coin_sig: dp.coin_sig, + denom_pub_hash: dp.h_denom, + ub_sig: dp.ub_sig, + }); + } const merchantContractSigResp = await cryptoApi.signContractTermsHash({ contractTermsHash, merchantPriv: args.merchantPriv, }); const requestBody: ExchangeBatchDepositRequest = { - coins: [ - { - contribution: Amounts.stringify(dp.contribution), - coin_pub: dp.coin_pub, - coin_sig: dp.coin_sig, - denom_pub_hash: dp.h_denom, - ub_sig: dp.ub_sig, - }, - ], + coins: depositCoins, merchant_sig: merchantContractSigResp.sig, merchant_payto_uri: depositPayto, wire_salt: wireSalt, @@ -276,7 +310,7 @@ export async function depositCoin(args: { refund_deadline: refundDeadline, merchant_pub: merchantPub, }; - const url = new URL(`batch-deposit`, dp.exchange_url); + const url = new URL(`batch-deposit`, args.exchangeBaseUrl); const httpResp = await http.fetch(url.href, { method: "POST", body: requestBody,