From 4fa88007f958796d7fe65d0fe4f6f45fcf953887 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 7 Apr 2021 19:29:51 +0200 Subject: get coin re-selection after accidental double spending to work --- .../src/operations/backup/export.ts | 54 +++++++++-- .../src/operations/backup/import.ts | 39 +++++++- .../src/operations/backup/index.ts | 18 ++-- .../taler-wallet-core/src/operations/deposits.ts | 4 +- .../taler-wallet-core/src/operations/exchanges.ts | 2 +- packages/taler-wallet-core/src/operations/pay.ts | 103 ++++++++++++++++++++- .../taler-wallet-core/src/operations/refresh.ts | 2 +- .../taler-wallet-core/src/operations/reserves.ts | 48 ++++++++-- packages/taler-wallet-core/src/operations/tip.ts | 2 +- .../taler-wallet-core/src/operations/withdraw.ts | 27 +++++- 10 files changed, 259 insertions(+), 40 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index c6e24289f..07c7b9ece 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -14,15 +14,6 @@ GNU Taler; see the file COPYING. If not, see */ -import { hash } from "../../crypto/primitives/nacl-fast"; -import { WalletBackupContentV1, BackupExchange, BackupCoin, BackupDenomination, BackupReserve, BackupPurchase, BackupProposal, BackupRefreshGroup, BackupBackupProvider, BackupTip, BackupRecoupGroup, BackupWithdrawalGroup, BackupBackupProviderTerms, BackupCoinSource, BackupCoinSourceType, BackupExchangeWireFee, BackupRefundItem, BackupRefundState, BackupProposalStatus, BackupRefreshOldCoin, BackupRefreshSession } from "@gnu-taler/taler-util"; -import { canonicalizeBaseUrl, canonicalJson } from "../../util/helpers"; -import { InternalWalletState } from "../state"; -import { provideBackupState, getWalletBackupState, WALLET_BACKUP_STATE_KEY } from "./state"; -import { Amounts, getTimestampNow } from "@gnu-taler/taler-util"; -import { Stores, CoinSourceType, CoinStatus, RefundState, AbortStatus, ProposalStatus } from "../../db.js"; -import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js"; - /** * Implementation of wallet backups (export/import/upload) and sync * server management. @@ -30,6 +21,51 @@ import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js"; * @author Florian Dold */ +/** + * Imports. + */ +import { hash } from "../../crypto/primitives/nacl-fast"; +import { + WalletBackupContentV1, + BackupExchange, + BackupCoin, + BackupDenomination, + BackupReserve, + BackupPurchase, + BackupProposal, + BackupRefreshGroup, + BackupBackupProvider, + BackupTip, + BackupRecoupGroup, + BackupWithdrawalGroup, + BackupBackupProviderTerms, + BackupCoinSource, + BackupCoinSourceType, + BackupExchangeWireFee, + BackupRefundItem, + BackupRefundState, + BackupProposalStatus, + BackupRefreshOldCoin, + BackupRefreshSession, +} from "@gnu-taler/taler-util"; +import { InternalWalletState } from "../state"; +import { + provideBackupState, + getWalletBackupState, + WALLET_BACKUP_STATE_KEY, +} from "./state"; +import { Amounts, getTimestampNow } from "@gnu-taler/taler-util"; +import { + Stores, + CoinSourceType, + CoinStatus, + RefundState, + AbortStatus, + ProposalStatus, +} from "../../db.js"; +import { encodeCrock, stringToBytes, getRandomBytes } from "../../index.js"; +import { canonicalizeBaseUrl, canonicalJson } from "@gnu-taler/taler-util"; + export async function exportBackup( ws: InternalWalletState, ): Promise { diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 05b6da084..e0ae379ab 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -14,11 +14,42 @@ GNU Taler; see the file COPYING. If not, see */ -import { BackupPurchase, AmountJson, Amounts, BackupDenomSel, WalletBackupContentV1, getTimestampNow, BackupCoinSourceType, BackupProposalStatus, codecForContractTerms, BackupRefundState, RefreshReason, BackupRefreshReason } from "@gnu-taler/taler-util"; -import { Stores, WalletContractData, DenomSelectionState, ExchangeWireInfo, ExchangeUpdateStatus, DenominationStatus, CoinSource, CoinSourceType, CoinStatus, ReserveBankInfo, ReserveRecordStatus, ProposalDownload, ProposalStatus, WalletRefundItem, RefundState, AbortStatus, RefreshSessionRecord } from "../../db.js"; +import { + BackupPurchase, + AmountJson, + Amounts, + BackupDenomSel, + WalletBackupContentV1, + getTimestampNow, + BackupCoinSourceType, + BackupProposalStatus, + codecForContractTerms, + BackupRefundState, + RefreshReason, + BackupRefreshReason, +} from "@gnu-taler/taler-util"; +import { + Stores, + WalletContractData, + DenomSelectionState, + ExchangeWireInfo, + ExchangeUpdateStatus, + DenominationStatus, + CoinSource, + CoinSourceType, + CoinStatus, + ReserveBankInfo, + ReserveRecordStatus, + ProposalDownload, + ProposalStatus, + WalletRefundItem, + RefundState, + AbortStatus, + RefreshSessionRecord, +} from "../../db.js"; import { TransactionHandle } from "../../index.js"; import { PayCoinSelection } from "../../util/coinSelection"; -import { j2s } from "../../util/helpers"; +import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { Logger } from "../../util/logging"; import { initRetryInfo } from "../../util/retries"; @@ -271,6 +302,8 @@ export async function importBackup( denomPubHash, ]); if (!existingDenom) { + logger.info(`importing backup denomination: ${j2s(backupDenomination)}`); + await tx.put(Stores.denominations, { denomPub: backupDenomination.denom_pub, denomPubHash: denomPubHash, diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 77a3219a5..49129d7de 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -25,13 +25,14 @@ * Imports. */ import { InternalWalletState } from "../state"; -import { AmountString, BackupRecovery, codecForAmountString, WalletBackupContentV1 } from "@gnu-taler/taler-util"; -import { TransactionHandle } from "../../util/query"; import { - BackupProviderRecord, - ConfigRecord, - Stores, -} from "../../db.js"; + AmountString, + BackupRecovery, + codecForAmountString, + WalletBackupContentV1, +} from "@gnu-taler/taler-util"; +import { TransactionHandle } from "../../util/query"; +import { BackupProviderRecord, ConfigRecord, Stores } from "../../db.js"; import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants"; import { bytesToString, @@ -43,7 +44,7 @@ import { rsaBlind, stringToBytes, } from "../../crypto/talerCrypto"; -import { canonicalizeBaseUrl, canonicalJson, j2s } from "../../util/helpers"; +import { canonicalizeBaseUrl, canonicalJson, j2s } from "@gnu-taler/taler-util"; import { durationAdd, durationFromSpec, @@ -408,6 +409,9 @@ export async function runBackupCycle(ws: InternalWalletState): Promise { const providers = await ws.db.iter(Stores.backupProviders).toArray(); logger.trace("got backup providers", providers); const backupJson = await exportBackup(ws); + + logger.trace(`running backup cycle with backup JSON: ${j2s(backupJson)}`); + const backupConfig = await provideBackupState(ws); const encBackup = await encryptBackup(backupConfig, backupJson); diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 6bb4f3d59..4c87f122f 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -21,7 +21,7 @@ import { stringToBytes, } from "../crypto/talerCrypto"; import { selectPayCoins } from "../util/coinSelection"; -import { canonicalJson } from "../util/helpers"; +import { canonicalJson } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow } from "../util/http"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { @@ -433,4 +433,4 @@ export async function createDepositGroup( await ws.db.put(Stores.depositGroups, depositGroup); return { depositGroupId }; -} \ No newline at end of file +} diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 08c554160..f48b08ff7 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -48,7 +48,7 @@ import { getExpiryTimestamp, readSuccessResponseTextOrThrow, } from "../index.js"; -import { j2s, canonicalizeBaseUrl } from "../util/helpers.js"; +import { j2s, canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { checkDbInvariant } from "../util/invariants.js"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries.js"; import { diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index da3980565..1e93f413b 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -83,8 +83,9 @@ import { CoinCandidateSelection, AvailableCoinInfo, selectPayCoins, + PreviousPayCoins, } from "../util/coinSelection.js"; -import { canonicalJson } from "../util/helpers.js"; +import { canonicalJson, j2s } from "@gnu-taler/taler-util"; import { initRetryInfo, updateRetryInfoTimeout, @@ -350,6 +351,13 @@ export async function applyCoinSpend( if (!coin) { throw Error("coin allocated for payment doesn't exist anymore"); } + if (coin.status !== CoinStatus.Fresh) { + // applyCoinSpend was called again, probably + // because of a coin re-selection to recover after + // accidental double spending. + // Ignore coins we already marked as spent. + continue; + } coin.status = CoinStatus.Dormant; const remaining = Amounts.sub( coin.currentAmount, @@ -867,7 +875,7 @@ async function storePayReplaySuccess( * * We do this by going through the coin history provided by the exchange and * (1) verifying the signatures from the exchange - * (2) adjusting the remaining coin value + * (2) adjusting the remaining coin value and refreshing it * (3) re-do coin selection with the bad coin removed */ async function handleInsufficientFunds( @@ -875,12 +883,99 @@ async function handleInsufficientFunds( proposalId: string, err: TalerErrorDetails, ): Promise { + logger.trace("handling insufficient funds, trying to re-select coins"); + const proposal = await ws.db.get(Stores.purchases, proposalId); if (!proposal) { return; } - throw Error("payment re-denomination not implemented yet"); + const brokenCoinPub = (err as any).coin_pub; + + const exchangeReply = (err as any).exchange_reply; + if ( + exchangeReply.code !== TalerErrorCode.EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS + ) { + // FIXME: set as failed + throw Error("can't handle error code"); + } + + logger.trace(`got error details: ${j2s(err)}`); + + const { contractData } = proposal.download; + + const candidates = await getCandidatePayCoins(ws, { + allowedAuditors: contractData.allowedAuditors, + allowedExchanges: contractData.allowedExchanges, + amount: contractData.amount, + maxDepositFee: contractData.maxDepositFee, + maxWireFee: contractData.maxWireFee, + timestamp: contractData.timestamp, + wireFeeAmortization: contractData.wireFeeAmortization, + wireMethod: contractData.wireMethod, + }); + + const prevPayCoins: PreviousPayCoins = []; + + for (let i = 0; i < proposal.payCoinSelection.coinPubs.length; i++) { + const coinPub = proposal.payCoinSelection.coinPubs[i]; + if (coinPub === brokenCoinPub) { + continue; + } + const contrib = proposal.payCoinSelection.coinContributions[i]; + const coin = await ws.db.get(Stores.coins, coinPub); + if (!coin) { + continue; + } + const denom = await ws.db.get(Stores.denominations, [ + coin.exchangeBaseUrl, + coin.denomPubHash, + ]); + if (!denom) { + continue; + } + prevPayCoins.push({ + coinPub, + contribution: contrib, + exchangeBaseUrl: coin.exchangeBaseUrl, + feeDeposit: denom.feeDeposit, + }); + } + + const res = selectPayCoins({ + candidates, + contractTermsAmount: contractData.amount, + depositFeeLimit: contractData.maxDepositFee, + wireFeeAmortization: contractData.wireFeeAmortization ?? 1, + wireFeeLimit: contractData.maxWireFee, + prevPayCoins, + }); + + if (!res) { + logger.trace("insufficient funds for coin re-selection"); + return; + } + + logger.trace("re-selected coins"); + + await ws.db.runWithWriteTransaction( + [ + Stores.purchases, + Stores.coins, + Stores.denominations, + Stores.refreshGroups, + ], + async (tx) => { + const p = await tx.get(Stores.purchases, proposalId); + if (!p) { + return; + } + p.payCoinSelection = res; + p.coinDepositPermissions = undefined; + await tx.put(Stores.purchases, p); + await applyCoinSpend(ws, tx, res); + }, + ); } /** @@ -973,7 +1068,7 @@ async function submitPay( message: "unexpected exception", hint: "unexpected exception", details: { - exception: e, + exception: e.toString(), }, }); }); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index d82ff946e..84460fb88 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -34,7 +34,7 @@ import { TalerErrorDetails, } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { amountToPretty } from "../util/helpers"; +import { amountToPretty } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow } from "../util/http"; import { checkDbInvariant } from "../util/invariants"; import { Logger } from "../util/logging"; diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index fe6f323c8..9467287a7 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -33,15 +33,46 @@ import { addPaytoQueryParams, } from "@gnu-taler/taler-util"; import { randomBytes } from "../crypto/primitives/nacl-fast.js"; -import { Stores, ReserveRecordStatus, ReserveBankInfo, ReserveRecord, CurrencyRecord, WithdrawalGroupRecord } from "../db.js"; -import { Logger, encodeCrock, getRandomBytes, readSuccessResponseJsonOrThrow, URL, readSuccessResponseJsonOrErrorCode, throwUnexpectedRequestError, TransactionHandle } from "../index.js"; +import { + Stores, + ReserveRecordStatus, + ReserveBankInfo, + ReserveRecord, + CurrencyRecord, + WithdrawalGroupRecord, +} from "../db.js"; +import { + Logger, + encodeCrock, + getRandomBytes, + readSuccessResponseJsonOrThrow, + URL, + readSuccessResponseJsonOrErrorCode, + throwUnexpectedRequestError, + TransactionHandle, +} from "../index.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; -import { canonicalizeBaseUrl } from "../util/helpers.js"; -import { initRetryInfo, getRetryDuration, updateRetryInfoTimeout } from "../util/retries.js"; +import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; +import { + initRetryInfo, + getRetryDuration, + updateRetryInfoTimeout, +} from "../util/retries.js"; import { guardOperationException, OperationFailedError } from "./errors.js"; -import { updateExchangeFromUrl, getExchangeTrust, getExchangePaytoUri } from "./exchanges.js"; +import { + updateExchangeFromUrl, + getExchangeTrust, + getExchangePaytoUri, +} from "./exchanges.js"; import { InternalWalletState } from "./state.js"; -import { updateWithdrawalDenoms, getCandidateWithdrawalDenoms, selectWithdrawalDenominations, denomSelectionInfoToState, processWithdrawGroup, getBankWithdrawalInfo } from "./withdraw.js"; +import { + updateWithdrawalDenoms, + getCandidateWithdrawalDenoms, + selectWithdrawalDenominations, + denomSelectionInfoToState, + processWithdrawGroup, + getBankWithdrawalInfo, +} from "./withdraw.js"; const logger = new Logger("reserves.ts"); @@ -488,7 +519,10 @@ async function updateReserve( const currency = balance.currency; await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); - const denoms = await getCandidateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); + const denoms = await getCandidateWithdrawalDenoms( + ws, + reserve.exchangeBaseUrl, + ); const newWithdrawalGroup = await ws.db.runWithWriteTransaction( [Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves], diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 5ea92912b..cc5274647 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -45,7 +45,7 @@ import { getRandomBytes, getHttpResponseErrorDetails, } from "../index.js"; -import { j2s } from "../util/helpers.js"; +import { j2s } from "@gnu-taler/taler-util"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { guardOperationException, makeErrorDetails } from "./errors.js"; diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 0c1acf8ec..fcaa0e6d5 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019-2020 Taler Systems SA + (C) 2019-2021 Taler Systems SA 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 @@ -14,7 +14,15 @@ GNU Taler; see the file COPYING. If not, see */ -import { AmountJson, Amounts, parseWithdrawUri, Timestamp } from "@gnu-taler/taler-util"; +/** + * Imports. + */ +import { + AmountJson, + Amounts, + parseWithdrawUri, + Timestamp, +} from "@gnu-taler/taler-util"; import { DenominationRecord, Stores, @@ -67,15 +75,17 @@ import { TalerErrorCode } from "@gnu-taler/taler-util"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries"; import { compare } from "@gnu-taler/taler-util"; +/** + * Logger for this file. + */ const logger = new Logger("withdraw.ts"); - /** * Information about what will happen when creating a reserve. * * Sent to the wallet frontend to be rendered and shown to the user. */ - interface ExchangeWithdrawDetails { +interface ExchangeWithdrawDetails { /** * Exchange that the reserve will be created at. */ @@ -631,6 +641,8 @@ export async function updateWithdrawalDenoms( logger.error("exchange details not available"); throw Error(`exchange ${exchangeBaseUrl} details not available`); } + // First do a pass where the validity of candidate denominations + // is checked and the result is stored in the database. const denominations = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl); for (const denom of denominations) { if (denom.status === DenominationStatus.Unverified) { @@ -639,6 +651,9 @@ export async function updateWithdrawalDenoms( exchangeDetails.masterPublicKey, ); if (!valid) { + logger.warn( + `Signature check for denomination h=${denom.denomPubHash} failed`, + ); denom.status = DenominationStatus.VerifiedBad; } else { denom.status = DenominationStatus.VerifiedGood; @@ -648,11 +663,13 @@ export async function updateWithdrawalDenoms( } // FIXME: This debug info should either be made conditional on some flag // or put into some wallet-core API. - logger.trace("updated withdrawable denominations"); const nextDenominations = await getCandidateWithdrawalDenoms( ws, exchangeBaseUrl, ); + logger.trace( + `updated withdrawable denominations for "${exchangeBaseUrl}, n=${nextDenominations.length}"`, + ); const now = getTimestampNow(); for (const denom of nextDenominations) { const startDelay = getDurationRemaining(denom.stampStart, now); -- cgit v1.2.3