summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-03-15 17:51:05 +0100
committerFlorian Dold <florian@dold.me>2022-03-15 17:51:11 +0100
commitc0be242292a770c4dbe6d5ed86343014d14e9a33 (patch)
treedef9dce47a95b32fde09ea64541ca43c56bc0d2a /packages
parenteb18c1f179cb9abadc25c14c39b28e2786fa6f43 (diff)
downloadwallet-core-c0be242292a770c4dbe6d5ed86343014d14e9a33.tar.gz
wallet-core-c0be242292a770c4dbe6d5ed86343014d14e9a33.tar.bz2
wallet-core-c0be242292a770c4dbe6d5ed86343014d14e9a33.zip
wallet: db-less benchmarking
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-util/src/walletTypes.ts10
-rw-r--r--packages/taler-wallet-cli/src/bench2.ts105
-rw-r--r--packages/taler-wallet-cli/src/index.ts25
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts274
-rw-r--r--packages/taler-wallet-core/src/bank-api-client.ts5
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts5
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts3
-rw-r--r--packages/taler-wallet-core/src/dbless.ts369
-rw-r--r--packages/taler-wallet-core/src/index.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts156
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: {