diff options
author | Florian Dold <florian@dold.me> | 2022-03-15 17:51:05 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-03-15 17:51:11 +0100 |
commit | c0be242292a770c4dbe6d5ed86343014d14e9a33 (patch) | |
tree | def9dce47a95b32fde09ea64541ca43c56bc0d2a | |
parent | eb18c1f179cb9abadc25c14c39b28e2786fa6f43 (diff) |
wallet: db-less benchmarking
-rw-r--r-- | packages/taler-util/src/walletTypes.ts | 10 | ||||
-rw-r--r-- | packages/taler-wallet-cli/src/bench2.ts | 105 | ||||
-rw-r--r-- | packages/taler-wallet-cli/src/index.ts | 25 | ||||
-rw-r--r-- | packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts | 274 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/bank-api-client.ts | 5 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/cryptoTypes.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts | 5 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/dbless.ts | 369 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/index.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refresh.ts | 156 |
11 files changed, 592 insertions, 364 deletions
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 1c2037977..9a3f1f8f8 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -458,6 +458,16 @@ export interface TalerErrorDetails { details: unknown; } +/** + * Minimal information needed about a planchet for unblinding a signature. + * + * Can be a withdrawal/tipping/refresh planchet. + */ +export interface PlanchetUnblindInfo { + denomPub: DenominationPubKey; + blindingKey: string; +} + export interface WithdrawalPlanchet { coinPub: string; coinPriv: string; diff --git a/packages/taler-wallet-cli/src/bench2.ts b/packages/taler-wallet-cli/src/bench2.ts index 884708207..43c28882e 100644 --- a/packages/taler-wallet-cli/src/bench2.ts +++ b/packages/taler-wallet-cli/src/bench2.ts @@ -22,16 +22,20 @@ import { codecForNumber, codecForString, codecOptional, - j2s, Logger, } from "@gnu-taler/taler-util"; import { - getDefaultNodeWallet2, - NodeHttpLib, - WalletApiOperation, - Wallet, - AccessStats, + checkReserve, + createFakebankReserve, + CryptoApi, + depositCoin, downloadExchangeInfo, + findDenomOrThrow, + generateReserveKeypair, + NodeHttpLib, + refreshCoin, + SynchronousCryptoWorkerFactory, + withdrawCoin, } from "@gnu-taler/taler-wallet-core"; /** @@ -44,15 +48,79 @@ export async function runBench2(configJson: any): Promise<void> { const logger = new Logger("Bench1"); // Validate the configuration file for this benchmark. - const benchConf = codecForBench1Config().decode(configJson); + const benchConf = codecForBench2Config().decode(configJson); + const curr = benchConf.currency; + const cryptoApi = new CryptoApi(new SynchronousCryptoWorkerFactory()); + + const http = new NodeHttpLib(); + http.setThrottling(false); + + const numIter = benchConf.iterations ?? 1; + const numDeposits = benchConf.deposits ?? 5; + + const reserveAmount = (numDeposits + 1) * 10; + + for (let i = 0; i < numIter; i++) { + const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http); + + const reserveKeyPair = generateReserveKeypair(); + + console.log("creating fakebank reserve"); + + await createFakebankReserve({ + amount: `${curr}:${reserveAmount}`, + exchangeInfo, + fakebankBaseUrl: benchConf.bank, + http, + reservePub: reserveKeyPair.reservePub, + }); + + console.log("waiting for reserve"); + + await checkReserve(http, benchConf.exchange, reserveKeyPair.reservePub); - const myHttpLib = new NodeHttpLib(); - myHttpLib.setThrottling(false); + console.log("reserve found"); - const exchangeInfo = await downloadExchangeInfo( - benchConf.exchange, - myHttpLib, - ); + const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8`); + + for (let j = 0; j < numDeposits; j++) { + console.log("withdrawing coin"); + const coin = await withdrawCoin({ + http, + cryptoApi, + reserveKeyPair, + denom: d1, + exchangeBaseUrl: benchConf.exchange, + }); + + console.log("depositing coin"); + + await depositCoin({ + amount: `${curr}:4`, + coin: coin, + cryptoApi, + exchangeBaseUrl: benchConf.exchange, + http, + depositPayto: benchConf.payto, + }); + + const refreshDenoms = [ + findDenomOrThrow(exchangeInfo, `${curr}:1`), + findDenomOrThrow(exchangeInfo, `${curr}:1`), + ]; + + console.log("refreshing coin"); + + await refreshCoin({ + oldCoin: coin, + cryptoApi, + http, + newDenoms: refreshDenoms, + }); + + console.log("refresh done"); + } + } } /** @@ -83,18 +151,12 @@ interface Bench2Config { currency: string; deposits?: number; - - /** - * How any iterations run until the wallet db gets purged - * Defaults to 20. - */ - restartAfter?: number; } /** * Schema validation codec for Bench1Config. */ -const codecForBench1Config = () => +const codecForBench2Config = () => buildCodecForObject<Bench2Config>() .property("bank", codecForString()) .property("payto", codecForString()) @@ -102,5 +164,4 @@ const codecForBench1Config = () => .property("iterations", codecOptional(codecForNumber())) .property("deposits", codecOptional(codecForNumber())) .property("currency", codecForString()) - .property("restartAfter", codecOptional(codecForNumber())) - .build("Bench1Config"); + .build("Bench2Config"); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 3b72f74b7..f754ca915 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -62,6 +62,7 @@ import { lintExchangeDeployment } from "./lint.js"; import { runBench1 } from "./bench1.js"; import { runEnv1 } from "./env1.js"; import { GlobalTestState, runTestWithState } from "./harness/harness.js"; +import { runBench2 } from "./bench2.js"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -168,8 +169,7 @@ export const walletCli = clk }, }) .maybeOption("inhibit", ["--inhibit"], clk.STRING, { - help: - "Inhibit running certain operations, useful for debugging and testing.", + help: "Inhibit running certain operations, useful for debugging and testing.", }) .flag("noThrottle", ["--no-throttle"], { help: "Don't do any request throttling.", @@ -559,8 +559,7 @@ backupCli.subcommand("status", "status").action(async (args) => { backupCli .subcommand("recoveryLoad", "load-recovery") .maybeOption("strategy", ["--strategy"], clk.STRING, { - help: - "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", + help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", }) .action(async (args) => { await withWallet(args, async (wallet) => { @@ -636,8 +635,7 @@ depositCli }); const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { - help: - "Subcommands for advanced operations (only use if you know what you're doing!).", + help: "Subcommands for advanced operations (only use if you know what you're doing!).", }); advancedCli @@ -656,6 +654,21 @@ advancedCli }); advancedCli + .subcommand("bench2", "bench2", { + help: "Run the 'bench2' benchmark", + }) + .requiredOption("configJson", ["--config-json"], clk.STRING) + .action(async (args) => { + let config: any; + try { + config = JSON.parse(args.bench2.configJson); + } catch (e) { + console.log("Could not parse config JSON"); + } + await runBench2(config); + }); + +advancedCli .subcommand("env1", "env1", { help: "Run a test environment for bench1", }) diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts index 9ff605df5..93c22af70 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts @@ -17,277 +17,24 @@ /** * Imports. */ +import { j2s } from "@gnu-taler/taler-util"; import { - AmountJson, - AmountLike, - Amounts, - AmountString, - codecForBankWithdrawalOperationPostResponse, - codecForDepositSuccess, - codecForExchangeMeltResponse, - codecForWithdrawResponse, - DenominationPubKey, - eddsaGetPublic, - encodeCrock, - ExchangeMeltRequest, - ExchangeProtocolVersion, - ExchangeWithdrawRequest, - getRandomBytes, - getTimestampNow, - hashWire, - j2s, - Timestamp, - UnblindedSignature, -} from "@gnu-taler/taler-util"; -import { - BankAccessApi, - BankApi, - BankServiceHandle, + checkReserve, CryptoApi, - DenominationRecord, + depositCoin, downloadExchangeInfo, - ExchangeInfo, - getBankWithdrawalInfo, - HttpRequestLibrary, - isWithdrawableDenom, + findDenomOrThrow, + generateReserveKeypair, NodeHttpLib, OperationFailedError, - readSuccessResponseJsonOrThrow, + refreshCoin, SynchronousCryptoWorkerFactory, + topupReserveWithDemobank, + withdrawCoin, } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; -const httpLib = new NodeHttpLib(); - -export interface ReserveKeypair { - reservePub: string; - reservePriv: string; -} - -/** - * Denormalized info about a coin. - */ -export interface CoinInfo { - coinPub: string; - coinPriv: string; - exchangeBaseUrl: string; - denomSig: UnblindedSignature; - denomPub: DenominationPubKey; - denomPubHash: string; - feeDeposit: string; - feeRefresh: string; -} - -export function generateReserveKeypair(): ReserveKeypair { - const priv = getRandomBytes(32); - const pub = eddsaGetPublic(priv); - return { - reservePriv: encodeCrock(priv), - reservePub: encodeCrock(pub), - }; -} - -async function topupReserveWithDemobank( - reservePub: string, - bankBaseUrl: string, - exchangeInfo: ExchangeInfo, - amount: AmountString, -) { - const bankHandle: BankServiceHandle = { - baseUrl: bankBaseUrl, - http: httpLib, - }; - const bankUser = await BankApi.createRandomBankUser(bankHandle); - const wopi = await BankAccessApi.createWithdrawalOperation( - bankHandle, - bankUser, - amount, - ); - const bankInfo = await getBankWithdrawalInfo( - httpLib, - wopi.taler_withdraw_uri, - ); - const bankStatusUrl = bankInfo.extractedStatusUrl; - if (!bankInfo.suggestedExchange) { - throw Error("no suggested exchange"); - } - const plainPaytoUris = - exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? []; - if (plainPaytoUris.length <= 0) { - throw new Error(); - } - const httpResp = await httpLib.postJson(bankStatusUrl, { - reserve_pub: reservePub, - selected_exchange: plainPaytoUris[0], - }); - await readSuccessResponseJsonOrThrow( - httpResp, - codecForBankWithdrawalOperationPostResponse(), - ); - await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi); -} - -async function withdrawCoin(args: { - http: HttpRequestLibrary; - cryptoApi: CryptoApi; - reserveKeyPair: ReserveKeypair; - denom: DenominationRecord; - exchangeBaseUrl: string; -}): Promise<CoinInfo> { - const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args; - const planchet = await cryptoApi.createPlanchet({ - coinIndex: 0, - denomPub: denom.denomPub, - feeWithdraw: denom.feeWithdraw, - reservePriv: reserveKeyPair.reservePriv, - reservePub: reserveKeyPair.reservePub, - secretSeed: encodeCrock(getRandomBytes(32)), - value: denom.value, - }); - - const reqBody: ExchangeWithdrawRequest = { - denom_pub_hash: planchet.denomPubHash, - reserve_sig: planchet.withdrawSig, - coin_ev: planchet.coinEv, - }; - const reqUrl = new URL( - `reserves/${planchet.reservePub}/withdraw`, - exchangeBaseUrl, - ).href; - - const resp = await http.postJson(reqUrl, reqBody); - const r = await readSuccessResponseJsonOrThrow( - resp, - codecForWithdrawResponse(), - ); - - const ubSig = await cryptoApi.unblindDenominationSignature({ - planchet, - evSig: r.ev_sig, - }); - - return { - coinPriv: planchet.coinPriv, - coinPub: planchet.coinPub, - denomSig: ubSig, - denomPub: denom.denomPub, - denomPubHash: denom.denomPubHash, - feeDeposit: Amounts.stringify(denom.feeDeposit), - feeRefresh: Amounts.stringify(denom.feeRefresh), - exchangeBaseUrl: args.exchangeBaseUrl, - }; -} - -function findDenomOrThrow( - exchangeInfo: ExchangeInfo, - amount: AmountString, -): DenominationRecord { - for (const d of exchangeInfo.keys.currentDenominations) { - if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) { - return d; - } - } - throw new Error("no matching denomination found"); -} - -async function depositCoin(args: { - http: HttpRequestLibrary; - cryptoApi: CryptoApi; - exchangeBaseUrl: string; - coin: CoinInfo; - amount: AmountString; -}) { - const { coin, http, cryptoApi } = args; - const depositPayto = "payto://x-taler-bank/localhost/foo"; - const wireSalt = encodeCrock(getRandomBytes(16)); - const contractTermsHash = encodeCrock(getRandomBytes(64)); - const depositTimestamp = getTimestampNow(); - const refundDeadline = getTimestampNow(); - const merchantPub = encodeCrock(getRandomBytes(32)); - 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), - }); - const requestBody = { - contribution: Amounts.stringify(dp.contribution), - merchant_payto_uri: depositPayto, - wire_salt: wireSalt, - h_contract_terms: contractTermsHash, - ub_sig: coin.denomSig, - timestamp: depositTimestamp, - wire_transfer_deadline: getTimestampNow(), - refund_deadline: refundDeadline, - coin_sig: dp.coin_sig, - denom_pub_hash: dp.h_denom, - merchant_pub: merchantPub, - }; - const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url); - const httpResp = await http.postJson(url.href, requestBody); - await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); -} - -async function refreshCoin(req: { - http: HttpRequestLibrary; - cryptoApi: CryptoApi; - oldCoin: CoinInfo; - newDenoms: DenominationRecord[]; -}): Promise<void> { - const { cryptoApi, oldCoin, http } = req; - const refreshSessionSeed = encodeCrock(getRandomBytes(32)); - const session = await cryptoApi.deriveRefreshSession({ - exchangeProtocolVersion: ExchangeProtocolVersion.V12, - feeRefresh: Amounts.parseOrThrow(oldCoin.feeRefresh), - kappa: 3, - meltCoinDenomPubHash: oldCoin.denomPubHash, - meltCoinPriv: oldCoin.coinPriv, - meltCoinPub: oldCoin.coinPub, - sessionSecretSeed: refreshSessionSeed, - newCoinDenoms: req.newDenoms.map((x) => ({ - count: 1, - denomPub: x.denomPub, - feeWithdraw: x.feeWithdraw, - value: x.value, - })), - }); - - const meltReqBody: ExchangeMeltRequest = { - coin_pub: oldCoin.coinPub, - confirm_sig: session.confirmSig, - denom_pub_hash: oldCoin.denomPubHash, - denom_sig: oldCoin.denomSig, - rc: session.hash, - value_with_fee: Amounts.stringify(session.meltValueWithFee), - }; - - const reqUrl = new URL( - `coins/${oldCoin.coinPub}/melt`, - oldCoin.exchangeBaseUrl, - ); - - const resp = await http.postJson(reqUrl.href, meltReqBody); - - const meltResponse = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeMeltResponse(), - ); - - const norevealIndex = meltResponse.noreveal_index; - - -} - /** * Run test for basic, bank-integrated withdrawal and payment. */ @@ -307,6 +54,7 @@ export async function runWalletDblessTest(t: GlobalTestState) { const reserveKeyPair = generateReserveKeypair(); await topupReserveWithDemobank( + http, reserveKeyPair.reservePub, bank.baseUrl, exchangeInfo, @@ -315,6 +63,8 @@ export async function runWalletDblessTest(t: GlobalTestState) { await exchange.runWirewatchOnce(); + await checkReserve(http, exchange.baseUrl, reserveKeyPair.reservePub); + const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8"); const coin = await withdrawCoin({ @@ -338,7 +88,7 @@ export async function runWalletDblessTest(t: GlobalTestState) { findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"), ]; - const freshCoins = await refreshCoin({ + await refreshCoin({ oldCoin: coin, cryptoApi, http, diff --git a/packages/taler-wallet-core/src/bank-api-client.ts b/packages/taler-wallet-core/src/bank-api-client.ts index 744c3b833..a61ea2eef 100644 --- a/packages/taler-wallet-core/src/bank-api-client.ts +++ b/packages/taler-wallet-core/src/bank-api-client.ts @@ -28,6 +28,8 @@ import { codecForString, encodeCrock, getRandomBytes, + j2s, + Logger, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -35,6 +37,8 @@ import { readSuccessResponseJsonOrThrow, } from "./index.browser.js"; +const logger = new Logger("bank-api-client.ts"); + export enum CreditDebitIndicator { Credit = "credit", Debit = "debit", @@ -98,6 +102,7 @@ export namespace BankApi { const resp = await bank.http.postJson(url.href, { username, password }); let paytoUri = `payto://x-taler-bank/localhost/${username}`; if (resp.status !== 200 && resp.status !== 202) { + logger.error(`${j2s(await resp.json())}`) throw new Error(); } try { diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 00a7fba81..3b3396046 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -42,6 +42,7 @@ export interface RefreshNewDenomInfo { value: AmountJson; feeWithdraw: AmountJson; denomPub: DenominationPubKey; + denomPubHash: string; } /** diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts index b5a5950b1..820397346 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts @@ -30,6 +30,7 @@ import { BlindedDenominationSignature, CoinDepositPermission, CoinEnvelope, + PlanchetUnblindInfo, RecoupRefreshRequest, RecoupRequest, UnblindedSignature, @@ -206,7 +207,7 @@ export class CryptoApi { } }; ws.terminationTimerHandle = timer.after(15 * 1000, destroy); - //ws.terminationTimerHandle.unref(); + ws.terminationTimerHandle.unref(); } handleWorkerError(ws: WorkerState, e: any): void { @@ -331,7 +332,7 @@ export class CryptoApi { } unblindDenominationSignature(req: { - planchet: WithdrawalPlanchet; + planchet: PlanchetUnblindInfo; evSig: BlindedDenominationSignature; }): Promise<UnblindedSignature> { return this.doRpc<UnblindedSignature>( diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts index 15a086ae1..b51d499d5 100644 --- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts @@ -73,6 +73,7 @@ import { BlindedDenominationSignature, RsaUnblindedSignature, UnblindedSignature, + PlanchetUnblindInfo, } from "@gnu-taler/taler-util"; import bigint from "big-integer"; import { DenominationRecord, WireFee } from "../../db.js"; @@ -432,7 +433,7 @@ export class CryptoImplementation { } unblindDenominationSignature(req: { - planchet: WithdrawalPlanchet; + planchet: PlanchetUnblindInfo; evSig: BlindedDenominationSignature; }): UnblindedSignature { if (req.evSig.cipher === DenomKeyType.Rsa) { diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts new file mode 100644 index 000000000..85a72e28d --- /dev/null +++ b/packages/taler-wallet-core/src/dbless.ts @@ -0,0 +1,369 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Helper functions to run wallet functionality (withdrawal, deposit, refresh) + * without a database or retry loop. + * + * Used for benchmarking, where we want to benchmark the exchange, but the + * normal wallet would be too sluggish. + */ + +/** + * Imports. + */ +import { + Amounts, + AmountString, + codecForAny, + codecForBankWithdrawalOperationPostResponse, + codecForDepositSuccess, + codecForExchangeMeltResponse, + codecForExchangeRevealResponse, + codecForWithdrawResponse, + DenominationPubKey, + eddsaGetPublic, + encodeCrock, + ExchangeMeltRequest, + ExchangeProtocolVersion, + ExchangeWithdrawRequest, + getRandomBytes, + getTimestampNow, + hashWire, + Logger, + parsePaytoUri, + UnblindedSignature, +} from "@gnu-taler/taler-util"; +import { DenominationRecord } from "./db.js"; +import { + assembleRefreshRevealRequest, + CryptoApi, + ExchangeInfo, + getBankWithdrawalInfo, + HttpRequestLibrary, + isWithdrawableDenom, + readSuccessResponseJsonOrThrow, +} from "./index.browser.js"; +import { BankAccessApi, BankApi, BankServiceHandle } from "./index.js"; + +const logger = new Logger("dbless.ts"); + +export interface ReserveKeypair { + reservePub: string; + reservePriv: string; +} + +/** + * Denormalized info about a coin. + */ +export interface CoinInfo { + coinPub: string; + coinPriv: string; + exchangeBaseUrl: string; + denomSig: UnblindedSignature; + denomPub: DenominationPubKey; + denomPubHash: string; + feeDeposit: string; + feeRefresh: string; +} + +export function generateReserveKeypair(): ReserveKeypair { + const priv = getRandomBytes(32); + const pub = eddsaGetPublic(priv); + return { + reservePriv: encodeCrock(priv), + reservePub: encodeCrock(pub), + }; +} + +/** + * Check the status of a reserve, use long-polling to wait + * until the reserve actually has been created. + */ +export async function checkReserve( + http: HttpRequestLibrary, + exchangeBaseUrl: string, + reservePub: string, + longpollTimeoutMs: number = 500, +): Promise<void> { + const reqUrl = new URL(`reserves/${reservePub}`, exchangeBaseUrl); + if (longpollTimeoutMs) { + reqUrl.searchParams.set("timeout_ms", `${longpollTimeoutMs}`); + } + const resp = await http.get(reqUrl.href); + if (resp.status !== 200) { + throw new Error("reserve not okay"); + } +} + +export async function topupReserveWithDemobank( + http: HttpRequestLibrary, + reservePub: string, + bankBaseUrl: string, + exchangeInfo: ExchangeInfo, + amount: AmountString, +) { + const bankHandle: BankServiceHandle = { + baseUrl: bankBaseUrl, + http, + }; + const bankUser = await BankApi.createRandomBankUser(bankHandle); + const wopi = await BankAccessApi.createWithdrawalOperation( + bankHandle, + bankUser, + amount, + ); + const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri); + const bankStatusUrl = bankInfo.extractedStatusUrl; + if (!bankInfo.suggestedExchange) { + throw Error("no suggested exchange"); + } + const plainPaytoUris = + exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? []; + if (plainPaytoUris.length <= 0) { + throw new Error(); + } + const httpResp = await http.postJson(bankStatusUrl, { + reserve_pub: reservePub, + selected_exchange: plainPaytoUris[0], + }); + await readSuccessResponseJsonOrThrow( + httpResp, + codecForBankWithdrawalOperationPostResponse(), + ); + await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi); +} + +export async function withdrawCoin(args: { + http: HttpRequestLibrary; + cryptoApi: CryptoApi; + reserveKeyPair: ReserveKeypair; + denom: DenominationRecord; + exchangeBaseUrl: string; +}): Promise<CoinInfo> { + const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args; + const planchet = await cryptoApi.createPlanchet({ + coinIndex: 0, + denomPub: denom.denomPub, + feeWithdraw: denom.feeWithdraw, + reservePriv: reserveKeyPair.reservePriv, + reservePub: reserveKeyPair.reservePub, + secretSeed: encodeCrock(getRandomBytes(32)), + value: denom.value, + }); + + const reqBody: ExchangeWithdrawRequest = { + denom_pub_hash: planchet.denomPubHash, + reserve_sig: planchet.withdrawSig, + coin_ev: planchet.coinEv, + }; + const reqUrl = new URL( + `reserves/${planchet.reservePub}/withdraw`, + exchangeBaseUrl, + ).href; + + const resp = await http.postJson(reqUrl, reqBody); + const r = await readSuccessResponseJsonOrThrow( + resp, + codecForWithdrawResponse(), + ); + + const ubSig = await cryptoApi.unblindDenominationSignature({ + planchet, + evSig: r.ev_sig, + }); + + return { + coinPriv: planchet.coinPriv, + coinPub: planchet.coinPub, + denomSig: ubSig, + denomPub: denom.denomPub, + denomPubHash: denom.denomPubHash, + feeDeposit: Amounts.stringify(denom.feeDeposit), + feeRefresh: Amounts.stringify(denom.feeRefresh), + exchangeBaseUrl: args.exchangeBaseUrl, + }; +} + +export function findDenomOrThrow( + exchangeInfo: ExchangeInfo, + amount: AmountString, +): DenominationRecord { + for (const d of exchangeInfo.keys.currentDenominations) { + if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) { + return d; + } + } + throw new Error("no matching denomination found"); +} + +export async function depositCoin(args: { + http: HttpRequestLibrary; + cryptoApi: CryptoApi; + exchangeBaseUrl: string; + coin: CoinInfo; + amount: AmountString; + depositPayto?: string; +}) { + const { coin, http, cryptoApi } = args; + const depositPayto = + args.depositPayto ?? "payto://x-taler-bank/localhost/foo"; + const wireSalt = encodeCrock(getRandomBytes(16)); + const contractTermsHash = encodeCrock(getRandomBytes(64)); + const depositTimestamp = getTimestampNow(); + const refundDeadline = getTimestampNow(); + const merchantPub = encodeCrock(getRandomBytes(32)); + 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), + }); + const requestBody = { + contribution: Amounts.stringify(dp.contribution), + merchant_payto_uri: depositPayto, + wire_salt: wireSalt, + h_contract_terms: contractTermsHash, + ub_sig: coin.denomSig, + timestamp: depositTimestamp, + wire_transfer_deadline: getTimestampNow(), + refund_deadline: refundDeadline, + coin_sig: dp.coin_sig, + denom_pub_hash: dp.h_denom, + merchant_pub: merchantPub, + }; + const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url); + const httpResp = await http.postJson(url.href, requestBody); + await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); +} + +export async function refreshCoin(req: { + http: HttpRequestLibrary; + cryptoApi: CryptoApi; + oldCoin: CoinInfo; + newDenoms: DenominationRecord[]; +}): Promise<void> { + const { cryptoApi, oldCoin, http } = req; + const refreshSessionSeed = encodeCrock(getRandomBytes(32)); + const session = await cryptoApi.deriveRefreshSession({ + exchangeProtocolVersion: ExchangeProtocolVersion.V12, + feeRefresh: Amounts.parseOrThrow(oldCoin.feeRefresh), + kappa: 3, + meltCoinDenomPubHash: oldCoin.denomPubHash, + meltCoinPriv: oldCoin.coinPriv, + meltCoinPub: oldCoin.coinPub, + sessionSecretSeed: refreshSessionSeed, + newCoinDenoms: req.newDenoms.map((x) => ({ + count: 1, + denomPub: x.denomPub, + denomPubHash: x.denomPubHash, + feeWithdraw: x.feeWithdraw, + value: x.value, + })), + }); + + const meltReqBody: ExchangeMeltRequest = { + coin_pub: oldCoin.coinPub, + confirm_sig: session.confirmSig, + denom_pub_hash: oldCoin.denomPubHash, + denom_sig: oldCoin.denomSig, + rc: session.hash, + value_with_fee: Amounts.stringify(session.meltValueWithFee), + }; + + logger.info("requesting melt"); + + const meltReqUrl = new URL( + `coins/${oldCoin.coinPub}/melt`, + oldCoin.exchangeBaseUrl, + ); + + logger.info("requesting melt done"); + + const meltHttpResp = await http.postJson(meltReqUrl.href, meltReqBody); + + const meltResponse = await readSuccessResponseJsonOrThrow( + meltHttpResp, + codecForExchangeMeltResponse(), + ); + + const norevealIndex = meltResponse.noreveal_index; + + const revealRequest = await assembleRefreshRevealRequest({ + cryptoApi, + derived: session, + newDenoms: req.newDenoms.map((x) => ({ + count: 1, + denomPubHash: x.denomPubHash, + })), + norevealIndex, + oldCoinPriv: oldCoin.coinPriv, + oldCoinPub: oldCoin.coinPub, + }); + + logger.info("requesting reveal"); + const reqUrl = new URL( + `refreshes/${session.hash}/reveal`, + oldCoin.exchangeBaseUrl, + ); + + const revealResp = await http.postJson(reqUrl.href, revealRequest); + + logger.info("requesting reveal done"); + + const reveal = await readSuccessResponseJsonOrThrow( + revealResp, + codecForExchangeRevealResponse(), + ); + + // We could unblind here, but we only use this function to + // benchmark the exchange. +} + +export async function createFakebankReserve(args: { + http: HttpRequestLibrary; + fakebankBaseUrl: string; + amount: string; + reservePub: string; + exchangeInfo: ExchangeInfo; +}): Promise<void> { + const { http, fakebankBaseUrl, amount, reservePub } = args; + const paytoUri = args.exchangeInfo.wire.accounts[0].payto_uri; + const pt = parsePaytoUri(paytoUri); + if (!pt) { + throw Error("failed to parse payto URI"); + } + const components = pt.targetPath.split("/"); + const creditorAcct = components[components.length - 1]; + const fbReq = await http.postJson( + new URL(`${creditorAcct}/admin/add-incoming`, fakebankBaseUrl).href, + { + amount, + reserve_pub: reservePub, + debit_account: "payto://x-taler-bank/localhost/testdebtor", + }, + ); + const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny()); +} diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index cc01e914e..93430732a 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -54,4 +54,7 @@ export * from "./bank-api-client.js"; export * from "./operations/reserves.js"; export * from "./operations/withdraw.js"; +export * from "./operations/refresh.js"; + +export * from "./dbless.js"; diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index cc2a1c566..8b6d8b2e4 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -15,6 +15,7 @@ */ import { + CoinPublicKeyString, DenomKeyType, encodeCrock, ExchangeMeltRequest, @@ -79,8 +80,12 @@ import { isWithdrawableDenom, selectWithdrawalDenominations, } from "./withdraw.js"; -import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js"; +import { + DerivedRefreshSession, + RefreshNewDenomInfo, +} from "../crypto/cryptoTypes.js"; import { GetReadWriteAccess } from "../util/query.js"; +import { CryptoApi } from "../index.browser.js"; const logger = new Logger("refresh.ts"); @@ -357,6 +362,7 @@ async function refreshMelt( newCoinDenoms.push({ count: dh.count, denomPub: newDenom.denomPub, + denomPubHash: newDenom.denomPubHash, feeWithdraw: newDenom.feeWithdraw, value: newDenom.value, }); @@ -472,6 +478,62 @@ async function refreshMelt( }); } +export async function assembleRefreshRevealRequest(args: { + cryptoApi: CryptoApi; + derived: DerivedRefreshSession; + norevealIndex: number; + oldCoinPub: CoinPublicKeyString; + oldCoinPriv: string; + newDenoms: { + denomPubHash: string; + count: number; + }[]; +}): Promise<ExchangeRefreshRevealRequest> { + const { + derived, + norevealIndex, + cryptoApi, + oldCoinPriv, + oldCoinPub, + newDenoms, + } = args; + const privs = Array.from(derived.transferPrivs); + privs.splice(norevealIndex, 1); + + const planchets = derived.planchetsForGammas[norevealIndex]; + if (!planchets) { + throw Error("refresh index error"); + } + + const newDenomsFlat: string[] = []; + const linkSigs: string[] = []; + + for (let i = 0; i < newDenoms.length; i++) { + const dsel = newDenoms[i]; + for (let j = 0; j < dsel.count; j++) { + const newCoinIndex = linkSigs.length; + const linkSig = await cryptoApi.signCoinLink( + oldCoinPriv, + dsel.denomPubHash, + oldCoinPub, + derived.transferPubs[norevealIndex], + planchets[newCoinIndex].coinEv, + ); + linkSigs.push(linkSig); + newDenomsFlat.push(dsel.denomPubHash); + } + } + + const req: ExchangeRefreshRevealRequest = { + coin_evs: planchets.map((x) => x.coinEv), + new_denoms_h: newDenomsFlat, + transfer_privs: privs, + transfer_pub: derived.transferPubs[norevealIndex], + link_sigs: linkSigs, + }; + return req; +} + async function refreshReveal( ws: InternalWalletState, refreshGroupId: string, @@ -527,6 +589,7 @@ async function refreshReveal( newCoinDenoms.push({ count: dh.count, denomPub: newDenom.denomPub, + denomPubHash: newDenom.denomPubHash, feeWithdraw: newDenom.feeWithdraw, value: newDenom.value, }); @@ -575,46 +638,20 @@ async function refreshReveal( sessionSecretSeed: refreshSession.sessionSecretSeed, }); - const privs = Array.from(derived.transferPrivs); - privs.splice(norevealIndex, 1); - - const planchets = derived.planchetsForGammas[norevealIndex]; - if (!planchets) { - throw Error("refresh index error"); - } - - const newDenomsFlat: string[] = []; - const linkSigs: string[] = []; - - for (let i = 0; i < refreshSession.newDenoms.length; i++) { - const dsel = refreshSession.newDenoms[i]; - for (let j = 0; j < dsel.count; j++) { - const newCoinIndex = linkSigs.length; - const linkSig = await ws.cryptoApi.signCoinLink( - oldCoin.coinPriv, - dsel.denomPubHash, - oldCoin.coinPub, - derived.transferPubs[norevealIndex], - planchets[newCoinIndex].coinEv, - ); - linkSigs.push(linkSig); - newDenomsFlat.push(dsel.denomPubHash); - } - } - - const req: ExchangeRefreshRevealRequest = { - coin_evs: planchets.map((x) => x.coinEv), - new_denoms_h: newDenomsFlat, - transfer_privs: privs, - transfer_pub: derived.transferPubs[norevealIndex], - link_sigs: linkSigs, - }; - const reqUrl = new URL( `refreshes/${derived.hash}/reveal`, oldCoin.exchangeBaseUrl, ); + const req = await assembleRefreshRevealRequest({ + cryptoApi: ws.cryptoApi, + derived, + newDenoms: newCoinDenoms, + norevealIndex: norevealIndex, + oldCoinPriv: oldCoin.coinPriv, + oldCoinPub: oldCoin.coinPub, + }); + const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => { return await ws.http.postJson(reqUrl.href, req, { timeout: getRefreshRequestTimeout(refreshGroup), @@ -629,51 +666,28 @@ async function refreshReveal( const coins: CoinRecord[] = []; for (let i = 0; i < refreshSession.newDenoms.length; i++) { + const ncd = newCoinDenoms[i]; for (let j = 0; j < refreshSession.newDenoms[i].count; j++) { const newCoinIndex = coins.length; - // FIXME: Look up in earlier transaction! - const denom = await ws.db - .mktx((x) => ({ - denominations: x.denominations, - })) - .runReadOnly(async (tx) => { - return tx.denominations.get([ - oldCoin.exchangeBaseUrl, - refreshSession.newDenoms[i].denomPubHash, - ]); - }); - if (!denom) { - console.error("denom not found"); - continue; - } const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex]; - if (denom.denomPub.cipher !== DenomKeyType.Rsa) { + if (ncd.denomPub.cipher !== DenomKeyType.Rsa) { throw Error("cipher unsupported"); } const evSig = reveal.ev_sigs[newCoinIndex].ev_sig; - let rsaSig: string; - if (typeof evSig === "string") { - rsaSig = evSig; - } else if (evSig.cipher === DenomKeyType.Rsa) { - rsaSig = evSig.blinded_rsa_signature; - } else { - throw Error("unsupported cipher"); - } - const denomSigRsa = await ws.cryptoApi.rsaUnblind( - rsaSig, - pc.blindingKey, - denom.denomPub.rsa_public_key, - ); + const denomSig = await ws.cryptoApi.unblindDenominationSignature({ + planchet: { + blindingKey: pc.blindingKey, + denomPub: ncd.denomPub, + }, + evSig, + }); const coin: CoinRecord = { blindingKey: pc.blindingKey, coinPriv: pc.coinPriv, coinPub: pc.coinPub, - currentAmount: denom.value, - denomPubHash: denom.denomPubHash, - denomSig: { - cipher: DenomKeyType.Rsa, - rsa_signature: denomSigRsa, - }, + currentAmount: ncd.value, + denomPubHash: ncd.denomPubHash, + denomSig, exchangeBaseUrl: oldCoin.exchangeBaseUrl, status: CoinStatus.Fresh, coinSource: { |