From 828e65b0eba66b7c999d9867396797a239449a6d Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 23 Aug 2021 22:28:36 +0200 Subject: fix un-offered denom situation, test case almost works --- .../src/integrationtests/harness.ts | 25 ++-- .../src/integrationtests/test-denom-unoffered.ts | 138 +++++++++++++++++++++ .../src/integrationtests/testrunner.ts | 2 + packages/taler-wallet-core/src/db.ts | 3 - packages/taler-wallet-core/src/index.ts | 1 + .../taler-wallet-core/src/operations/exchanges.ts | 43 ++++++- packages/taler-wallet-core/src/operations/pay.ts | 6 + .../src/operations/transactions.ts | 2 +- .../taler-wallet-core/src/operations/withdraw.ts | 17 ++- 9 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts (limited to 'packages') diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts index 10b93b330..d47dfe098 100644 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -838,12 +838,6 @@ export class ExchangeService implements ExchangeServiceInterface { e.roundUnit ?? `${e.currency}:0.01`, ); setTalerPaths(config, gc.testDir + "/talerhome"); - - config.setString( - "exchange", - "keydir", - "${TALER_DATA_HOME}/exchange/live-keys/", - ); config.setString( "exchange", "revocation_dir", @@ -1078,6 +1072,23 @@ export class ExchangeService implements ExchangeServiceInterface { ); } + async purgeSecmodKeys(): Promise { + const cfg = Configuration.load(this.configFilename); + const rsaKeydir = cfg.getPath("taler-exchange-secmod-rsa", "KEY_DIR").required(); + const eddsaKeydir = cfg.getPath("taler-exchange-secmod-eddsa", "KEY_DIR").required(); + // Be *VERY* careful when changing this, or you will accidentally delete user data. + await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); + await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); + } + + async purgeDatabase(): Promise { + await sh( + this.globalState, + "exchange-dbinit", + `taler-exchange-dbinit -r -c "${this.configFilename}"`, + ); + } + async start(): Promise { if (this.isRunning()) { throw Error("exchange is already running"); @@ -1111,8 +1122,6 @@ export class ExchangeService implements ExchangeServiceInterface { [ "-c", this.configFilename, - "--num-threads", - "1", ...this.timetravelArgArr, ], `exchange-httpd-${this.name}`, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts new file mode 100644 index 000000000..7a1ff669a --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts @@ -0,0 +1,138 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { + PreparePayResultType, + TalerErrorCode, + TalerErrorDetails, + TransactionType, +} from "@gnu-taler/taler-util"; +import { + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { makeEventId } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; + +export async function runDenomUnofferedTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Make the exchange forget the denomination. + // Effectively we completely reset the exchange, + // but keep the exchange master public key. + + await exchange.stop(); + await exchange.purgeDatabase(); + await exchange.purgeSecmodKeys(); + await exchange.start(); + await exchange.pingUntilAvailable(); + + await merchant.stop(); + await merchant.start(); + await merchant.pingUntilAvailable(); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + { + const orderResp = await MerchantPrivateApi.createOrder( + merchant, + "default", + { + order: order, + }, + ); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await wallet.client.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const exc = await t.assertThrowsAsync(async () => { + await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + }); + + const errorDetails: TalerErrorDetails = exc.operationError; + // FIXME: We might want a more specific error code here! + t.assertDeepEqual( + errorDetails.code, + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + ); + const merchantErrorCode = (errorDetails.details as any).errorResponse.code; + t.assertDeepEqual( + merchantErrorCode, + TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND, + ); + + const purchId = makeEventId(TransactionType.Payment, preparePayResult.proposalId); + await wallet.client.call(WalletApiOperation.DeleteTransaction, { + transactionId: purchId, + }); + + // Now, delete the purchase and refresh operation. + } + + await wallet.client.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + forceUpdate: true, + }); + + // Now withdrawal should work again. + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + await wallet.runUntilDone(); + + const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {}); + console.log(JSON.stringify(txs, undefined, 2)); +} + +runDenomUnofferedTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index ab699e8b5..25067fbbd 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -82,6 +82,7 @@ import { runPaymentForgettableTest } from "./test-payment-forgettable.js"; import { runPaymentZeroTest } from "./test-payment-zero.js"; import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; +import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; /** * Test runner. @@ -101,6 +102,7 @@ const allTests: TestMainFunction[] = [ runBankApiTest, runClaimLoopTest, runDepositTest, + runDenomUnofferedTest, runExchangeManagementTest, runExchangeTimetravelTest, runFeeRegressionTest, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 65a874ea0..093332e84 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -963,9 +963,6 @@ export interface RefreshSessionRecord { /** * 512-bit secret that can be used to derive * the other cryptographic material for the refresh session. - * - * FIXME: We currently store the derived material, but - * should always derive it. */ sessionSecretSeed: string; diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index ccd3488d9..035edc76b 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -46,3 +46,4 @@ export * from "./wallet-api-types.js"; export * from "./wallet.js"; export * from "./operations/backup/index.js"; +export { makeEventId } from "./operations/transactions.js"; diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 0670c8a61..23459de92 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -153,6 +153,9 @@ async function downloadExchangeWithTermsOfService( return { tosText, tosEtag }; } +/** + * Get exchange details from the database. + */ export async function getExchangeDetails( tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; @@ -320,6 +323,7 @@ interface ExchangeKeysDownloadResult { reserveClosingDelay: Duration; expiry: Timestamp; recoup: Recoup[]; + listIssueDate: Timestamp; } /** @@ -392,6 +396,7 @@ async function downloadKeysInfo( minDuration: durationFromSpec({ hours: 1 }), }), recoup: exchangeKeysJson.recoup ?? [], + listIssueDate: exchangeKeysJson.list_issue_date, }; } @@ -508,9 +513,9 @@ async function updateExchangeFromUrlImpl( r.lastError = undefined; r.retryInfo = initRetryInfo(); r.lastUpdate = getTimestampNow(); - (r.nextUpdate = keysInfo.expiry), - // New denominations might be available. - (r.nextRefreshCheck = getTimestampNow()); + r.nextUpdate = keysInfo.expiry; + // New denominations might be available. + r.nextRefreshCheck = getTimestampNow(); r.detailsPointer = { currency: details.currency, masterPublicKey: details.masterPublicKey, @@ -521,17 +526,47 @@ async function updateExchangeFromUrlImpl( await tx.exchangeDetails.put(details); logger.trace("updating denominations in database"); + const currentDenomSet = new Set( + keysInfo.currentDenominations.map((x) => x.denomPubHash), + ); for (const currentDenom of keysInfo.currentDenominations) { const oldDenom = await tx.denominations.get([ baseUrl, currentDenom.denomPubHash, ]); if (oldDenom) { - // FIXME: Do consistency check + // FIXME: Do consistency check, report to auditor if necessary. } else { await tx.denominations.put(currentDenom); } } + + // Update list issue date for all denominations, + // and mark non-offered denominations as such. + await tx.denominations.indexes.byExchangeBaseUrl + .iter(r.baseUrl) + .forEachAsync(async (x) => { + if (!currentDenomSet.has(x.denomPubHash)) { + // FIXME: Here, an auditor report should be created, unless + // the denomination is really legally expired. + if (x.isOffered) { + x.isOffered = false; + logger.info( + `setting denomination ${x.denomPubHash} to offered=false`, + ); + } + } else { + x.listIssueDate = keysInfo.listIssueDate; + if (!x.isOffered) { + x.isOffered = true; + logger.info( + `setting denomination ${x.denomPubHash} to offered=true`, + ); + } + } + await tx.denominations.put(x); + }); + logger.trace("done updating denominations in database"); // Handle recoup diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 54049feb2..a201e6f8d 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -211,6 +211,12 @@ function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean { if (coin.suspended) { return false; } + if (denom.isRevoked) { + return false; + } + if (!denom.isOffered) { + return false; + } if (coin.status !== CoinStatus.Fresh) { return false; } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index a07551e82..a21dbe8b8 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -505,7 +505,7 @@ export async function deleteTransaction( const purchase = await tx.purchases.get(proposalId); if (purchase) { found = true; - await tx.proposals.delete(proposalId); + await tx.purchases.delete(proposalId); } if (found) { await tx.tombstones.put({ diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 81c35c17b..521cfa113 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -158,8 +158,8 @@ interface ExchangeWithdrawDetails { } /** - * Check if a denom is withdrawable based on the expiration time - * and revocation state. + * Check if a denom is withdrawable based on the expiration time, + * revocation and offered state. */ export function isWithdrawableDenom(d: DenominationRecord): boolean { const now = getTimestampNow(); @@ -175,7 +175,7 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean { } const remaining = getDurationRemaining(lastPossibleWithdraw, now); const stillOkay = remaining.d_ms !== 0; - return started && stillOkay && !d.isRevoked; + return started && stillOkay && !d.isRevoked && d.isOffered; } /** @@ -230,6 +230,14 @@ export function selectWithdrawalDenominations( } } + if (logger.shouldLogTrace()) { + logger.trace(`selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`); + for (const sd of selectedDenoms) { + logger.trace(`denom_pub_hash=${sd.denom.denomPubHash}, count=${sd.count}`); + } + logger.trace("(end of withdrawal denom list)"); + } + return { selectedDenoms, totalCoinValue, @@ -306,7 +314,8 @@ export async function getCandidateWithdrawalDenoms( return await ws.db .mktx((x) => ({ denominations: x.denominations })) .runReadOnly(async (tx) => { - return tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); + const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); + return allDenoms.filter(isWithdrawableDenom); }); } -- cgit v1.2.3