wallet: db-less benchmarking

This commit is contained in:
Florian Dold 2022-03-15 17:51:05 +01:00
parent eb18c1f179
commit c0be242292
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 592 additions and 364 deletions

View File

@ -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;

View File

@ -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 myHttpLib = new NodeHttpLib();
myHttpLib.setThrottling(false);
const http = new NodeHttpLib();
http.setThrottling(false);
const exchangeInfo = await downloadExchangeInfo(
benchConf.exchange,
myHttpLib,
);
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);
console.log("reserve found");
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");

View File

@ -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
@ -655,6 +653,21 @@ advancedCli
await runBench1(config);
});
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",

View File

@ -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,

View File

@ -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 {

View File

@ -42,6 +42,7 @@ export interface RefreshNewDenomInfo {
value: AmountJson;
feeWithdraw: AmountJson;
denomPub: DenominationPubKey;
denomPubHash: string;
}
/**

View File

@ -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>(

View File

@ -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) {

View File

@ -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());
}

View File

@ -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";

View File

@ -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: {