From c22b13eebe0577c2b948a99e42670580d49d60ce Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 7 Mar 2024 17:28:14 +0100 Subject: wallet-core: implement and test lost flag for denominations --- .../src/integrationtests/test-denom-lost.ts | 81 ++++++++++++++++++++++ .../src/integrationtests/testrunner.ts | 2 + packages/taler-util/src/wallet-types.ts | 16 +++++ packages/taler-wallet-core/src/db.ts | 7 ++ packages/taler-wallet-core/src/denominations.ts | 5 +- packages/taler-wallet-core/src/exchanges.ts | 11 +++ packages/taler-wallet-core/src/wallet-api-types.ts | 13 ++++ packages/taler-wallet-core/src/wallet.ts | 25 +++++++ 8 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-denom-lost.ts (limited to 'packages') diff --git a/packages/taler-harness/src/integrationtests/test-denom-lost.ts b/packages/taler-harness/src/integrationtests/test-denom-lost.ts new file mode 100644 index 000000000..307ae352a --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-denom-lost.ts @@ -0,0 +1,81 @@ +/* + This file is part of GNU Taler + (C) 2020 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 + */ + +/** + * Imports. + */ +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, +} from "../harness/helpers.js"; + +/** + * Run test for refreshe after a payment. + */ +export async function runDenomLostTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await wres.withdrawalFinishedCond; + + const dsBefore = await walletClient.call( + WalletApiOperation.TestingGetDenomStats, + { + exchangeBaseUrl: exchange.baseUrl, + }, + ); + + t.assertDeepEqual(dsBefore.numLost, 0); + t.assertDeepEqual(dsBefore.numOffered, dsBefore.numKnown); + + await exchange.stop(); + + await exchange.purgeSecmodKeys(); + + await exchange.start(); + + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + + const dsAfter = await walletClient.call( + WalletApiOperation.TestingGetDenomStats, + { + exchangeBaseUrl: exchange.baseUrl, + }, + ); + + // All previous denominations were lost + t.assertDeepEqual(dsBefore.numOffered, dsAfter.numLost); + // But we have new ones! + t.assertTrue(dsAfter.numKnown > dsBefore.numKnown); +} + +runDenomLostTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index a294b27f4..566350770 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -36,6 +36,7 @@ import { runBankApiTest } from "./test-bank-api.js"; import { runClaimLoopTest } from "./test-claim-loop.js"; import { runClauseSchnorrTest } from "./test-clause-schnorr.js"; import { runCurrencyScopeTest } from "./test-currency-scope.js"; +import { runDenomLostTest } from "./test-denom-lost.js"; import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; import { runDepositTest } from "./test-deposit.js"; import { runExchangeDepositTest } from "./test-exchange-deposit.js"; @@ -208,6 +209,7 @@ const allTests: TestMainFunction[] = [ runWalletBalanceZeroTest, runWalletInsufficientBalanceTest, runWalletWirefeesTest, + runDenomLostTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 69811969c..0d2713fdd 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -2982,6 +2982,22 @@ export interface TestingWaitTransactionRequest { txState: TransactionState; } +export interface TestingGetDenomStatsRequest { + exchangeBaseUrl: string; +} + +export interface TestingGetDenomStatsResponse { + numKnown: number; + numOffered: number; + numLost: number; +} + +export const codecForTestingGetDenomStatsRequest = + (): Codec => + buildCodecForObject() + .property("exchangeBaseUrl", codecForString()) + .build("TestingGetDenomStatsRequest"); + export interface WithdrawalExchangeAccountDetails { /** * Payto URI to credit the exchange. diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 14621c2d5..b59efe034 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -485,6 +485,13 @@ export interface DenominationRecord { */ isRevoked: boolean; + /** + * If set to true, the exchange announced that the private key for this + * denomination is lost. Thus it can't be used to sign new coins + * during withdrawal/refresh/..., but the coins can still be spent. + */ + isLost?: boolean; + /** * Base URL of the exchange. */ diff --git a/packages/taler-wallet-core/src/denominations.ts b/packages/taler-wallet-core/src/denominations.ts index a539918de..d41307d5d 100644 --- a/packages/taler-wallet-core/src/denominations.ts +++ b/packages/taler-wallet-core/src/denominations.ts @@ -24,7 +24,6 @@ import { AmountString, DenominationInfo, Duration, - durationFromSpec, FeeDescription, FeeDescriptionPair, TalerProtocolTimestamp, @@ -471,10 +470,10 @@ export function isWithdrawableDenom( } else { lastPossibleWithdraw = AbsoluteTime.subtractDuraction( withdrawExpire, - durationFromSpec({ minutes: 5 }), + Duration.fromSpec({ minutes: 5 }), ); } const remaining = Duration.getRemaining(lastPossibleWithdraw, now); const stillOkay = remaining.d_ms !== 0; - return started && stillOkay && !d.isRevoked && d.isOffered; + return started && stillOkay && !d.isRevoked && d.isOffered && !d.isLost; } diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts index 335caff62..c44178de8 100644 --- a/packages/taler-wallet-core/src/exchanges.ts +++ b/packages/taler-wallet-core/src/exchanges.ts @@ -779,6 +779,7 @@ async function downloadExchangeKeysInfo( exchangeMasterPub: exchangeKeysJsonUnchecked.master_public_key, isOffered: true, isRevoked: false, + isLost: denomIn.lost ?? false, value: Amounts.stringify(value), currency: value.currency, stampExpireDeposit: timestampProtocolToDb( @@ -1432,6 +1433,16 @@ export async function updateExchangeFromUrlHandler( ]); if (oldDenom) { // FIXME: Do consistency check, report to auditor if necessary. + // See https://bugs.taler.net/n/8594 + + // Mark lost denominations as lost. + if (currentDenom.isLost && !oldDenom.isLost) { + logger.warn( + `marking denomination ${currentDenom.denomPubHash} of ${exchangeBaseUrl} as lost`, + ); + oldDenom.isLost = true; + await tx.denominations.put(currentDenom); + } } else { await tx.denominations.put(currentDenom); } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index ace702e88..4f4b24b62 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -117,6 +117,8 @@ import { StoredBackupList, TestPayArgs, TestPayResult, + TestingGetDenomStatsRequest, + TestingGetDenomStatsResponse, TestingListTasksForTransactionRequest, TestingListTasksForTransactionsResponse, TestingSetTimetravelRequest, @@ -255,6 +257,7 @@ export enum WalletApiOperation { RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", ListAssociatedRefreshes = "listAssociatedRefreshes", TestingListTaskForTransaction = "testingListTasksForTransaction", + TestingGetDenomStats = "testingGetDenomStats", } // group: Initialization @@ -1113,6 +1116,15 @@ export type TestingWaitTransactionStateOp = { response: EmptyObject; }; +/** + * Get stats about an exchange denomination. + */ +export type TestingGetDenomStatsOp = { + op: WalletApiOperation.TestingGetDenomStats; + request: TestingGetDenomStatsRequest; + response: TestingGetDenomStatsResponse; +}; + /** * Set a coin as (un-)suspended. * Suspended coins won't be used for payments. @@ -1238,6 +1250,7 @@ export type WalletOperations = { [WalletApiOperation.RemoveGlobalCurrencyExchange]: RemoveGlobalCurrencyExchangeOp; [WalletApiOperation.ListAssociatedRefreshes]: ListAssociatedRefreshesOp; [WalletApiOperation.TestingListTaskForTransaction]: TestingListTasksForTransactionOp; + [WalletApiOperation.TestingGetDenomStats]: TestingGetDenomStatsOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 8c9eee009..46f58ec81 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -57,6 +57,7 @@ import { TalerErrorCode, TalerProtocolTimestamp, TalerUriAction, + TestingGetDenomStatsResponse, TestingListTasksForTransactionsResponse, TestingWaitTransactionRequest, TimerAPI, @@ -126,6 +127,7 @@ import { codecForStartRefundQueryRequest, codecForSuspendTransaction, codecForTestPayArgs, + codecForTestingGetDenomStatsRequest, codecForTestingListTasksForTransactionRequest, codecForTestingSetTimetravelRequest, codecForTransactionByIdRequest, @@ -799,6 +801,29 @@ async function dispatchRequestInternal( }); return {}; } + case WalletApiOperation.TestingGetDenomStats: { + const req = codecForTestingGetDenomStatsRequest().decode(payload); + const denomStats: TestingGetDenomStatsResponse = { + numKnown: 0, + numLost: 0, + numOffered: 0, + }; + await wex.db.runReadOnlyTx(["denominations"], async (tx) => { + const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll( + req.exchangeBaseUrl, + ); + for (const d of denoms) { + denomStats.numKnown++; + if (d.isOffered) { + denomStats.numOffered++; + } + if (d.isLost) { + denomStats.numLost++; + } + } + }); + return denomStats; + } case WalletApiOperation.ListExchanges: { return await listExchanges(wex); } -- cgit v1.2.3