commit 46e8184ce90eeeeb2a4f6e15a5116de977e67d4a
parent 4d3a511087635e2b4d8389c861f1d3b75f158f78
Author: Antoine A <>
Date: Sat, 13 Jun 2026 11:45:25 +0200
nexus: add bench
Diffstat:
8 files changed, 900 insertions(+), 265 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1315,6 +1315,7 @@ dependencies = [
[[package]]
name = "http-client"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
dependencies = [
"compact_str",
"futures-util",
@@ -3469,6 +3470,7 @@ dependencies = [
[[package]]
name = "taler-api"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
dependencies = [
"aws-lc-rs",
"axum",
@@ -3495,10 +3497,12 @@ dependencies = [
[[package]]
name = "taler-build"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
[[package]]
name = "taler-common"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
dependencies = [
"anyhow",
"aws-lc-rs",
@@ -3527,6 +3531,7 @@ dependencies = [
[[package]]
name = "taler-macros"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
dependencies = [
"proc-macro2",
"quote",
@@ -3536,6 +3541,7 @@ dependencies = [
[[package]]
name = "taler-test-utils"
version = "1.5.0"
+source = "git+git://git.taler.net/taler-rust.git/#4d358b00b0bb55d20f58d18c3e2cac0a495cdd23"
dependencies = [
"aws-lc-rs",
"axum",
diff --git a/Cargo.toml b/Cargo.toml
@@ -47,15 +47,15 @@ zip = { version = "8.5", default-features = false, features = [
] }
tower-http = { version = "0.6", features = ["fs"]}
shlex = "2.0"
-#taler-common = { git = "git://git.taler.net/taler-rust.git/" }
-#taler-api = { git = "git://git.taler.net/taler-rust.git/" }
-#taler-build = { git = "git://git.taler.net/taler-rust.git/" }
-#taler-test-utils = { git = "git://git.taler.net/taler-rust.git/" }
-#taler-macros = { git = "git://git.taler.net/taler-rust.git/" }
-#http-client = { git = "git://git.taler.net/taler-rust.git/" }
-taler-common = { path = "../taler-rust/common/taler-common" }
-taler-api = { path = "../taler-rust/common/taler-api" }
-taler-build = { path = "../taler-rust/common/taler-build" }
-taler-test-utils = { path = "../taler-rust/common/taler-test-utils" }
-taler-macros = { path = "../taler-rust/common/taler-macros" }
-http-client = { path = "../taler-rust/common/http-client" }
-\ No newline at end of file
+taler-common = { git = "git://git.taler.net/taler-rust.git/" }
+taler-api = { git = "git://git.taler.net/taler-rust.git/" }
+taler-build = { git = "git://git.taler.net/taler-rust.git/" }
+taler-test-utils = { git = "git://git.taler.net/taler-rust.git/" }
+taler-macros = { git = "git://git.taler.net/taler-rust.git/" }
+http-client = { git = "git://git.taler.net/taler-rust.git/" }
+#taler-common = { path = "../taler-rust/common/taler-common" }
+#taler-api = { path = "../taler-rust/common/taler-api" }
+#taler-build = { path = "../taler-rust/common/taler-build" }
+#taler-test-utils = { path = "../taler-rust/common/taler-test-utils" }
+#taler-macros = { path = "../taler-rust/common/taler-macros" }
+#http-client = { path = "../taler-rust/common/http-client" }
+\ No newline at end of file
diff --git a/Makefile b/Makefile
@@ -64,7 +64,14 @@ deb:
ci:
contrib/ci/run-all-jobs.sh
-
.PHONY: fmt
fmt:
- rustfmt-unstable --apply
-\ No newline at end of file
+ rustfmt-unstable --apply
+
+.PHONY: bank-bench-db
+bank-bench-db: install-nobuild-files
+ cargo test -rp libeufin-bank bench_db -- --nocapture
+
+.PHONY: nexus-bench-db
+nexus-bench-db: install-nobuild-files
+ cargo test -rp libeufin-nexus bench_db -- --nocapture
+\ No newline at end of file
diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs
@@ -465,7 +465,7 @@ pub mod test {
}
}
- pub async fn cache_tokens(&mut self, usernames: &[&'static str]) {
+ pub async fn cache_tokens(&mut self, usernames: &[&str]) {
let tasks = usernames
.iter()
.map(|username| {
diff --git a/libeufin-bank/src/bench.rs b/libeufin-bank/src/bench.rs
@@ -0,0 +1,624 @@
+/*
+* This file is part of LibEuFin.
+* Copyright (C) 2026 Taler Systems S.A.
+
+* LibEuFin is free software; you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation; either version 3, or
+* (at your option) any later version.
+
+* LibEuFin 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 Affero General
+* Public License for more details.
+
+* You should have received a copy of the GNU Affero General Public
+* License along with LibEuFin; see the file COPYING. If not, see
+* <http://www.gnu.org/licenses/>
+*/
+
+use std::fmt::Write as _;
+
+use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
+use axum::http::header::AUTHORIZATION;
+use compact_str::{CompactString, format_compact};
+use jiff::{Timestamp, Zoned, tz::TimeZone};
+use taler_api::crypto::eddsa_sign;
+use taler_common::{
+ api::{EddsaPublicKey, HashCode, ShortHashCode, wire::TransferResponse},
+ bench::{Bench, h16, h32, h64},
+ encoding::hex,
+ error_code::ErrorCode,
+};
+use taler_test_utils::{json, server::TestServer};
+use uuid::Uuid;
+
+use crate::{
+ api::{
+ account::RegisterAccountResponse,
+ cashout::CashoutResponse,
+ conversion::ConversionRateClassResponse,
+ tan::ChallengeResponse,
+ test::{bank_setup, tan_code},
+ token::TokenSuccessResponse,
+ tx::TransactionCreateResponse,
+ withdrawal::BankAccountCreateWithdrawalResponse,
+ },
+ db::gc::collect,
+ pw::PwCrypto,
+};
+
+#[tokio::test]
+pub async fn bench_db() {
+ let mut ctx = bank_setup().await;
+ let mut b = Bench::new(&ctx.state.db);
+ // Skip 4 accounts created by bank_setup
+ let skip = 4;
+ // Customer account will be used in tests so we want to generate more data for him
+ let customer = 3;
+ let exchange = 2;
+ // In general half of the data is for generated account and half is for customer
+ let mid = b.amount / 2;
+
+ let password = PwCrypto::Bcrypt { cost: 4 }.hashpw("password");
+ let accounts_pubs: Vec<_> = (0..b.amount * 2)
+ .map(|_| {
+ let key_pair = Ed25519KeyPair::generate().unwrap();
+ let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
+ (key_pair, pub_key)
+ })
+ .collect();
+
+ b.table(
+ "customers(username, name, password_hash, cashout_payto)",
+ |f, i| {
+ writeln!(
+ f,
+ "account_{i}\t{password}\tMr n°{i}\t{}",
+ ctx.unknown_payto
+ )
+ },
+ )
+ .await;
+ b.table("conversion_rate_classes(name)", |f, i| {
+ writeln!(f, "Class n°{i}")
+ })
+ .await;
+ b.table(
+ "bank_accounts(internal_payto,owning_customer_id,is_public,conversion_rate_class_id)",
+ |f, i| {
+ let id = match i % 5 {
+ 0 => "\\N",
+ 1 | 2 => "1",
+ 3 => "2",
+ _ => &(i % 10).to_string(),
+ };
+ writeln!(
+ f,
+ "payto://x-taler-bank/localhost/account_{i}\t{}\t{}\t{id}",
+ i + skip,
+ i % 3 == 0
+ )
+ },
+ )
+ .await;
+ b.table(
+ "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)",
+ |f, i| {
+ let account = if i > mid { customer} else {i+4};
+ writeln!(
+ f,
+ "\\\\x{}\t0\t0\treadonly\tfalse\t{account}\t\\N\t0",
+ h32(),
+ )
+ },
+ )
+ .await;
+ b.table(
+ "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)",
+ |f, i| {
+ let account = if i > mid { customer} else {i+4};
+ writeln!(
+ f,
+ "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t{exchange}",
+ ctx.unknown_payto, ctx.unknown_payto
+ )?;
+ writeln!(
+ f,
+ "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t{account}",
+ ctx.unknown_payto, ctx.unknown_payto
+ )
+ },
+ )
+ .await;
+ b.table("bank_transaction_operations", |f, i| {
+ writeln!(f, "\\\\x{}\t{i}", h32())
+ })
+ .await;
+ b.table("tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)", |f, i| {
+ let account = if i > mid { customer} else {i+4};
+ writeln!(f, "{}\t\\\\x{}\t\\\\x{}\taccount_reconfig\tcode\t0\t0\t0\t{account}\tsms\tinfo", Uuid::new_v4(), h64(), h16())
+ })
+ .await;
+ b.table("taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)", |f, i| {
+ let account = if i > mid { customer} else {i+4};
+ writeln!(f, "{}\t{account}\t\\\\x{}\t0", Uuid::new_v4(), h32())
+ })
+ .await;
+ b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)", |f, i| {
+ let ty = if i%2==0 {"reserve"} else {"kyc"};
+ let recurrent = if i%3 == 0 {"true" } else {"false"};
+ let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)};
+
+ let key = hex::encode( accounts_pubs[i].1.as_ref());
+ let sig = h64();
+ writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t0\t{incoming_transaction_id}")
+ }).await;
+ b.table(
+ "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)",
+ |f, i| {
+ let key = hex::encode(accounts_pubs[i].1.as_ref());
+ writeln!(f, "{}\t{i}\t\\\\x{key}", i * 2)
+ },
+ )
+ .await;
+ b.table("taler_exchange_outgoing(bank_transaction)", |f, i| {
+ writeln!(f, "{}", i * 2 - 1)
+ })
+ .await;
+ b.table("transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)", |f, i| {
+ if i % 2 == 0 {
+ writeln!(f, "\\\\x{}\t\\\\x{}\t(42, 0)\thttps://url\t{i}\t{i}\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N", h32(), h64())
+ } else {
+ writeln!(f, "\\\\x{}\t\\\\x{}\t(42, 0)\thttps://url\t\\N\t{i}\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure", h32(), h64())
+ }
+ })
+ .await;
+ b.table(
+ "taler_exchange_incoming(type, metadata, bank_transaction)",
+ |f, i| {
+ let hex = h32();
+ let ty = if i % 2 == 0 { "reserve" } else { "kyc" };
+ writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2)
+ },
+ )
+ .await;
+ b.table("bank_stats(timeframe, start_time)", |f, i| {
+ writeln!(
+ f,
+ "day\t{}",
+ Zoned::new(Timestamp::from_second(i as i64).unwrap(), TimeZone::UTC).datetime()
+ )
+ })
+ .await;
+ b.table("cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)", |f, i| {
+ let account = if i > mid { customer} else {i+4};
+ writeln!(f, "\\\\x{}\t(0,0)\t(0,0)\tsubject\t0\t{account}\t{i}", h32())
+ })
+ .await;
+
+ // Prepare customer accounts
+ ctx.fill_cashout_info("customer").await;
+ ctx.set_max_debt("customer", "1000000").await;
+
+ // Accounts
+ let mut names = Vec::new();
+ b.measure("account_create", async |i| {
+ let username = format!("account_bench_{i}");
+ ctx.post("/accounts")
+ .json(json!({
+ "username": username,
+ "password": format!("account_bench_{i}-password"),
+ "name": format!("Bench Account {i}")
+ }))
+ .await
+ .assert_ok_json::<RegisterAccountResponse>();
+ names.push(username);
+ })
+ .await;
+ ctx.cache_tokens(&names.iter().map(|it| it.as_str()).collect::<Vec<_>>())
+ .await;
+ b.measure("account_reconfig", async |i| {
+ ctx.patcha(format!("/accounts/account_bench_{i}"))
+ .json(json!({
+ "name": format!("New Bench Account {i}")
+ }))
+ .await
+ .assert_no_content();
+ })
+ .await;
+ b.measure("account_reconfig_auth", async |i| {
+ ctx.patcha(format!("/accounts/account_bench_{i}/auth"))
+ .json(json!({
+ "old_password": format!("account_bench_{i}-password"),
+ "new_password": format!("account_bench_{i}-password"),
+ }))
+ .await
+ .assert_no_content();
+ })
+ .await;
+ b.measure("account_list", async |_| {
+ ctx.get_admin("/accounts").await.assert_ok()
+ })
+ .await;
+ b.measure("account_list_class", async |i| {
+ ctx.get_admin(format!("/accounts?conversion_rate_class={}", i % 10))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("account_list_name", async |_| {
+ ctx.get_admin("/accounts?name=Mr").await.assert_ok()
+ })
+ .await;
+ b.measure("account_list_public", async |_| {
+ ctx.get("/public-accounts").await.assert_ok()
+ })
+ .await;
+ b.measure("account_get", async |i| {
+ ctx.geta(format!("/accounts/account_bench_{i}"))
+ .await
+ .assert_ok()
+ })
+ .await;
+
+ // Token
+ let mut t = Vec::new();
+ b.measure("token_create", async |_| {
+ t.push(
+ ctx.postpw("/accounts/customer/token")
+ .json(json!({
+ "scope": "readonly",
+ "refreshable": true
+ }))
+ .await
+ .assert_ok_json::<TokenSuccessResponse>()
+ .access_token,
+ )
+ })
+ .await;
+ b.measure("token_refresh", async |i| {
+ ctx.post("/accounts/customer/token")
+ .header(AUTHORIZATION, format!("Bearer {}", t[i]))
+ .json(json!({
+ "scope": "readonly"
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("token_list", async |_| {
+ ctx.geta("/accounts/customer/tokens").await.assert_ok()
+ })
+ .await;
+ b.measure("token_delete", async |i| {
+ ctx.delete("/accounts/customer/token")
+ .header(AUTHORIZATION, format!("Bearer {}", t[i]))
+ .await
+ .assert_no_content();
+ })
+ .await;
+
+ // Conversion rate classes
+ let mut c = Vec::new();
+ b.measure("class_create", async |i| {
+ c.push(
+ ctx.post_admin("/conversion-rate-classes")
+ .json(json!({
+ "name": format!("Gen class {i}")
+ }))
+ .await
+ .assert_ok_json::<ConversionRateClassResponse>()
+ .conversion_rate_class_id,
+ )
+ })
+ .await;
+ b.measure("class_patch", async |i| {
+ ctx.patch_admin(format!("/conversion-rate-classes/{}", c[i]))
+ .json(json!({
+ "name": format!("Gen class {i}"),
+ "description": format!("test {i}")
+ }))
+ .await
+ .assert_no_content()
+ })
+ .await;
+ b.measure("class_get", async |i| {
+ ctx.get_admin(format!("/conversion-rate-classes/{}", c[i]))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("class_list", async |_| {
+ ctx.get_admin("/conversion-rate-classes").await.assert_ok()
+ })
+ .await;
+ b.measure("class_delete", async |i| {
+ ctx.delete_admin(format!("/conversion-rate-classes/{}", c[i]))
+ .await
+ .assert_no_content()
+ })
+ .await;
+
+ // Transaction
+ let mut tx = Vec::new();
+ b.measure("transaction_create", async |_| {
+ tx.push(
+ ctx.posta("/accounts/customer/transactions")
+ .json(json!({
+ "payto_uri": format!("{}?receiver-name=Test&message=payout", ctx.merchant_payto),
+ "amount": "KUDOS:0.001"
+ }))
+ .await
+ .assert_ok_json::<TransactionCreateResponse>()
+ .row_id,
+ )
+ })
+ .await;
+ b.measure("transaction_get", async |i| {
+ ctx.geta(format!("/accounts/customer/transactions/{}", tx[i]))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("transaction_history", async |_| {
+ ctx.geta("/accounts/customer/transactions")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("transaction_revenue", async |_| {
+ ctx.geta("/accounts/merchant/taler-revenue/history")
+ .await
+ .assert_ok()
+ })
+ .await;
+
+ // Withdrawal
+ let mut w = Vec::new();
+ b.measure("withdrawal_create", async |_| {
+ w.push(
+ ctx.posta("/accounts/customer/withdrawals")
+ .json(json!({
+ "amount": "KUDOS:0.0001"
+ }))
+ .await
+ .assert_ok_json::<BankAccountCreateWithdrawalResponse>()
+ .withdrawal_id,
+ )
+ })
+ .await;
+ b.measure("withdrawal_get", async |i| {
+ ctx.get(format!("/withdrawals/{}", w[i])).await.assert_ok()
+ })
+ .await;
+ b.measure("withdrawal_status", async |i| {
+ ctx.get(format!("/taler-integration/withdrawal-operation/{}", w[i]))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("withdrawal_select", async |i| {
+ ctx.post(format!("/taler-integration/withdrawal-operation/{}", w[i]))
+ .json(json!({
+ "reserve_pub": &EddsaPublicKey::rand(),
+ "selected_exchange": &ctx.exchange_payto
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("withdrawal_confirm", async |i| {
+ ctx.posta(format!("/accounts/customer/withdrawals/{}/confirm", w[i]))
+ .json(json!({}))
+ .await
+ .assert_no_content()
+ })
+ .await;
+ b.measure("withdrawal_abort", async |i| {
+ ctx.posta(format!("/accounts/customer/withdrawals/{}/abort", w[i]))
+ .await
+ .assert_error(ErrorCode::BANK_ABORT_CONFIRM_CONFLICT);
+ })
+ .await;
+
+ // Cashout
+ let mut c = Vec::new();
+ b.measure("cashout_create", async |_| {
+ c.push(
+ ctx.posta("/accounts/customer/cashouts")
+ .json(json!({
+ "request_uid": ShortHashCode::rand(),
+ "amount_debit": "KUDOS:0.1",
+ "amount_credit": &ctx.convert("KUDOS:0.1").await
+ }))
+ .await
+ .assert_ok_json::<CashoutResponse>()
+ .cashout_id,
+ )
+ })
+ .await;
+ b.measure("cashout_get", async |i| {
+ ctx.geta(format!("/accounts/customer/cashouts/{}", c[i]))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("cashout_history", async |_| {
+ ctx.geta("/accounts/customer/cashouts").await.assert_ok()
+ })
+ .await;
+ b.measure("cashout_history_admin", async |_| {
+ ctx.get_admin("/cashouts").await.assert_ok()
+ })
+ .await;
+
+ // Wire gateway
+ let mut t = Vec::new();
+ b.measure("wg_transfer", async |_| {
+ t.push(
+ ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
+ .json(json!({
+ "request_uid": HashCode::rand(),
+ "amount": "KUDOS:0.0001",
+ "exchange_base_url": "http://exchange.example.com/",
+ "wtid": ShortHashCode::rand(),
+ "credit_account": &ctx.customer_payto
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>()
+ .row_id,
+ )
+ })
+ .await;
+ b.measure("wg_transfer_get", async |i| {
+ ctx.geta(format!(
+ "/accounts/exchange/taler-wire-gateway/transfers/{}",
+ t[i]
+ ))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_transfer_page", async |_| {
+ ctx.geta("/accounts/exchange/taler-wire-gateway/transfers")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_transfer_page_filter", async |_| {
+ ctx.geta("/accounts/exchange/taler-wire-gateway/transfers?status=success")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_add", async |_| {
+ ctx.posta("/accounts/exchange/taler-wire-gateway/admin/add-incoming")
+ .json(json!({
+ "amount": "KUDOS:0.0001",
+ "reserve_pub": EddsaPublicKey::rand(),
+ "debit_account": &ctx.customer_payto
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_incoming", async |_| {
+ ctx.geta("/accounts/exchange/taler-wire-gateway/history/incoming")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_outgoing", async |_| {
+ ctx.geta("/accounts/exchange/taler-wire-gateway/history/outgoing")
+ .await
+ .assert_ok()
+ })
+ .await;
+
+ // TAN challenges
+ let mut c = Vec::new();
+ b.measure("tan_send", async |i| {
+ let res = ctx
+ .patcha(format!("/accounts/account_bench_{i}"))
+ .json(json!({
+ "contact_data" : {
+ "phone": "+99",
+ "email": "email@example.com"
+ },
+ "tan_channel": "sms"
+ }))
+ .await
+ .assert_accepted_json::<ChallengeResponse>();
+ let challenge = &res.challenges[0];
+ ctx.posta(format!(
+ "/accounts/account_bench_{i}/challenge/{}",
+ challenge.challenge_id
+ ))
+ .await
+ .assert_ok();
+ let code = tan_code(&challenge.tan_info);
+ c.push((challenge.challenge_id.clone(), code))
+ })
+ .await;
+ b.measure("tan_confirm", async |i| {
+ let (id, code) = &c[i];
+ ctx.posta(format!(
+ "/accounts/account_bench_{i}/challenge/{id}/confirm"
+ ))
+ .json(json!({
+ "tan": code
+ }))
+ .await
+ .assert_no_content();
+ })
+ .await;
+
+ // Revenue api
+ b.measure("transaction_revenue", async |_| {
+ ctx.geta("/accounts/exchange/taler-revenue/history")
+ .await
+ .assert_ok()
+ })
+ .await;
+
+ // Wire transfer
+ b.measure("wt_register", async |i| {
+ let (pair, key) = &accounts_pubs[i];
+ let body = json!({
+ "credit_account": &ctx.exchange_payto,
+ "credit_amount": "KUDOS:55",
+ "type": "reserve",
+ "alg": "EdDSA",
+ "account_pub": key,
+ "authorization_pub": key,
+ "authorization_sig": eddsa_sign(pair, key.as_ref()),
+ "recurrent":false
+ });
+ for _ in 0..2 {
+ ctx.post("/taler-prepared-transfer/registration")
+ .json(&body)
+ .await
+ .assert_ok()
+ }
+ })
+ .await;
+ b.measure("wt_unregister", async |i| {
+ let (pair, key) = &accounts_pubs[i];
+ let now = Timestamp::now().to_string();
+ let req = json!({
+ "timestamp": &now,
+ "authorization_pub": key,
+ "authorization_sig": eddsa_sign(pair, now.as_ref()),
+ });
+ ctx.post("/taler-prepared-transfer/unregistration")
+ .json(&req)
+ .await
+ .assert_no_content();
+ ctx.post("/taler-prepared-transfer/unregistration")
+ .json(&req)
+ .await
+ .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
+ })
+ .await;
+
+ // Delete accounts
+ b.measure("account_delete", async |i| {
+ ctx.deletea(format!("/accounts/account_bench_{i}"))
+ .await
+ .assert_no_content()
+ })
+ .await;
+
+ // Other
+ b.measure("monitor", async |_| {
+ ctx.get_admin("/monitor").await.assert_ok();
+ })
+ .await;
+ let now = Timestamp::MIN;
+ collect(&ctx.state.db, &now, &now, &now).await.unwrap();
+ b.measure("gc", async |_| {
+ collect(&ctx.state.db, &now, &now, &now).await.unwrap();
+ })
+ .await;
+}
diff --git a/libeufin-bank/src/lib.rs b/libeufin-bank/src/lib.rs
@@ -70,6 +70,8 @@ use crate::{
pub mod api;
pub mod auth;
+#[cfg(test)]
+pub mod bench;
pub mod config;
pub mod db;
pub mod mfa;
diff --git a/libeufin-nexus/src/bench.rs b/libeufin-nexus/src/bench.rs
@@ -17,274 +17,269 @@
* <http://www.gnu.org/licenses/>
*/
-#[cfg(test)]
-mod test {
- use std::fmt::Write as _;
+use std::fmt::Write as _;
- use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
- use compact_str::{CompactString, format_compact};
- use jiff::Timestamp;
- use serde_json::json;
- use taler_api::{crypto::eddsa_sign, subject::subject_fmt_qr_bill};
- use taler_common::{
- api::{EddsaPublicKey, HashCode, ShortHashCode},
- bench::{Bench, h32, h64},
- encoding::hex,
- error_code::ErrorCode,
- };
- use taler_test_utils::server::TestServer as _;
+use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
+use compact_str::{CompactString, format_compact};
+use jiff::Timestamp;
+use serde_json::json;
+use taler_api::{crypto::eddsa_sign, subject::subject_fmt_qr_bill};
+use taler_common::{
+ api::{EddsaPublicKey, HashCode, ShortHashCode},
+ bench::{Bench, h32, h64},
+ encoding::hex,
+ error_code::ErrorCode,
+};
+use taler_test_utils::server::TestServer as _;
- use crate::{
- api::test::api_setup,
- test::{
- CLIENT, EXCHANGE, incomplete_out, malformed_completeted_in, malformed_in,
- malformed_incomplete_in, malformed_out, prepared_completeted_in, prepared_in,
- prepared_incomplete_in, talerable_completeted_in, talerable_in,
- talerable_incomplete_in, talerable_out,
- },
- };
+use crate::{
+ api::test::api_setup,
+ test::{
+ CLIENT, EXCHANGE, incomplete_out, malformed_completeted_in, malformed_in,
+ malformed_incomplete_in, malformed_out, prepared_completeted_in, prepared_in,
+ prepared_incomplete_in, talerable_completeted_in, talerable_in, talerable_incomplete_in,
+ talerable_out,
+ },
+};
- #[tokio::test]
- pub async fn bench_db() {
- let (server, db) = api_setup().await;
- let amount = 10;
- let iter = 10;
- let amount = amount.max(10);
- let accounts_pubs: Vec<_> = (0..amount * 2)
- .map(|_| {
- let key_pair = Ed25519KeyPair::generate().unwrap();
- let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
- (key_pair, pub_key)
- })
- .collect();
- let mut b = Bench::new(&db, iter, amount);
- let uri = CLIENT.as_uri();
- b.table("incoming_transactions(amount, subject, execution_time, debit_payto, uetr, tx_id, acct_svcr_ref)", |f, i| {
- let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")};
+#[tokio::test]
+pub async fn bench_db() {
+ let (server, db) = api_setup().await;
+ let mut b = Bench::new(&db);
+ let accounts_pubs: Vec<_> = (0..b.amount * 2)
+ .map(|_| {
+ let key_pair = Ed25519KeyPair::generate().unwrap();
+ let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
+ (key_pair, pub_key)
+ })
+ .collect();
- if i % 3 == 0 {
- writeln!(f, "(20,0)\t{subject}\t0\t{uri}\t{}\t\\N\t\\N", uuid::Uuid::new_v4())?;
- writeln!(f, "(21,0)\t{subject}\t0\t{uri}\t\\N\tTX_ID{}\t\\N", i*2)?;
- writeln!(f, "(22,0)\t{subject}\t0\t{uri}\t\\N\t\\N\tREF{}", i*2)
- } else if i%3 == 1 {
- writeln!(f, "(30,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\t\\N", uuid::Uuid::new_v4(), i*2)?;
- writeln!(f, "(31,0)\t{subject}\t0\t{uri}\t\\N\tTX_ID{}\tREF{}", i*2+1, i*2)?;
- writeln!(f, "(32,0)\t{subject}\t0\t{uri}\t{}\t\\N\tREF{}", uuid::Uuid::new_v4(), i*2+1)
- } else {
- writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2, i*2)?;
- writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2+1, i*2+1)
- }
- }).await;
- b.table("outgoing_transactions(amount, subject, execution_time, credit_payto, end_to_end_id, acct_svcr_ref)", |f, i| {
- let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")};
+ let uri = CLIENT.as_uri();
+ b.table("incoming_transactions(amount, subject, execution_time, debit_payto, uetr, tx_id, acct_svcr_ref)", |f, i| {
+ let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")};
+ if i % 3 == 0 {
+ writeln!(f, "(20,0)\t{subject}\t0\t{uri}\t{}\t\\N\t\\N", uuid::Uuid::new_v4())?;
+ writeln!(f, "(21,0)\t{subject}\t0\t{uri}\t\\N\tTX_ID{}\t\\N", i*2)?;
+ writeln!(f, "(22,0)\t{subject}\t0\t{uri}\t\\N\t\\N\tREF{}", i*2)
+ } else if i%3 == 1 {
+ writeln!(f, "(30,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\t\\N", uuid::Uuid::new_v4(), i*2)?;
+ writeln!(f, "(31,0)\t{subject}\t0\t{uri}\t\\N\tTX_ID{}\tREF{}", i*2+1, i*2)?;
+ writeln!(f, "(32,0)\t{subject}\t0\t{uri}\t{}\t\\N\tREF{}", uuid::Uuid::new_v4(), i*2+1)
+ } else {
+ writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2, i*2)?;
+ writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2+1, i*2+1)
+ }
+ }).await;
+ b.table("outgoing_transactions(amount, subject, execution_time, credit_payto, end_to_end_id, acct_svcr_ref)", |f, i| {
+ let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")};
- if i % 2 == 0 {
- writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t\\N\tREF{}", i*2)?;
- writeln!(f, "(41,0)\t{subject}\t0\t{uri}\tE2E_ID{}\t\\N", i*2)
- } else {
- writeln!(f, "(40,0)\t{subject}\t0\t{uri}\tE2E_ID{}\tREF{}", i*2, i*2)?;
- writeln!(f, "(41,0)\t{subject}\t0\t{uri}\tE2E_ID{}\tREF{}", i*2+1, i*2+1)
- }
- }).await;
- b.table("initiated_outgoing_transactions(amount, subject, initiation_time, credit_payto, outgoing_transaction_id, end_to_end_id)", |f, i| {
- writeln!(f, "(42,0)\tsubject\t0\t{uri}\t{}\tE2E_ID{i}", i*2)
- }).await;
- b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, reference_number, registered_at, incoming_transaction_id)", |f, i| {
- let ty = if i%2==0 {"reserve"} else {"kyc"};
- let recurrent = if i%3 == 0 {"true" } else {"false"};
- let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)};
- let reference_number = subject_fmt_qr_bill(accounts_pubs[i].1.as_ref());
- let key = hex::encode( accounts_pubs[i].1.as_ref());
- let sig = h64();
- writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t{reference_number}\t0\t{incoming_transaction_id}")
- }).await;
- b.table(
- "pending_recurrent_incoming_transactions(incoming_transaction_id, authorization_pub)",
- |f, i| {
- let key = hex::encode(accounts_pubs[i].1.as_ref());
- writeln!(f, "{}\t\\\\x{key}", i * 2)
- },
- )
- .await;
- b.table(
- "bounced_transactions(incoming_transaction_id, initiated_outgoing_transaction_id)",
- |f, i| {
- if i % 10 == 0 {
- writeln!(f, "{}\t{}", i / 2, i / 2)
- } else {
- Ok(())
- }
- },
- )
- .await;
- b.table(
- "talerable_incoming_transactions(type, metadata, incoming_transaction_id)",
- |f, i| {
- let hex = h32();
- let ty = if i % 2 == 0 { "reserve" } else { "kyc" };
- writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2)
- },
- )
- .await;
- b.table(
- "talerable_outgoing_transactions(wtid, exchange_base_url, outgoing_transaction_id)",
- |f, i| {
- let hex = h32();
- writeln!(f, "\\\\x{hex}\thttp://exchange.example.com/\t{}", i * 2 - 1)
- },
- )
- .await;
- b.table("transfer_operations(initiated_outgoing_transaction_id, request_uid, wtid, exchange_base_url)", |f, i| {
+ if i % 2 == 0 {
+ writeln!(f, "(40,0)\t{subject}\t0\t{uri}\t\\N\tREF{}", i*2)?;
+ writeln!(f, "(41,0)\t{subject}\t0\t{uri}\tE2E_ID{}\t\\N", i*2)
+ } else {
+ writeln!(f, "(40,0)\t{subject}\t0\t{uri}\tE2E_ID{}\tREF{}", i*2, i*2)?;
+ writeln!(f, "(41,0)\t{subject}\t0\t{uri}\tE2E_ID{}\tREF{}", i*2+1, i*2+1)
+ }
+ }).await;
+ b.table("initiated_outgoing_transactions(amount, subject, initiation_time, credit_payto, outgoing_transaction_id, end_to_end_id)", |f, i| {
+ writeln!(f, "(42,0)\tsubject\t0\t{uri}\t{}\tE2E_ID{i}", i*2)
+ }).await;
+ b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, reference_number, registered_at, incoming_transaction_id)", |f, i| {
+ let ty = if i%2==0 {"reserve"} else {"kyc"};
+ let recurrent = if i%3 == 0 {"true" } else {"false"};
+ let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)};
+
+ let reference_number = subject_fmt_qr_bill(accounts_pubs[i].1.as_ref());
+ let key = hex::encode( accounts_pubs[i].1.as_ref());
+ let sig = h64();
+ writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t{reference_number}\t0\t{incoming_transaction_id}")
+ }).await;
+ b.table(
+ "pending_recurrent_incoming_transactions(incoming_transaction_id, authorization_pub)",
+ |f, i| {
+ let key = hex::encode(accounts_pubs[i].1.as_ref());
+ writeln!(f, "{}\t\\\\x{key}", i * 2)
+ },
+ )
+ .await;
+ b.table(
+ "bounced_transactions(incoming_transaction_id, initiated_outgoing_transaction_id)",
+ |f, i| {
+ if i % 10 == 0 {
+ writeln!(f, "{}\t{}", i / 2, i / 2)
+ } else {
+ Ok(())
+ }
+ },
+ )
+ .await;
+ b.table(
+ "talerable_incoming_transactions(type, metadata, incoming_transaction_id)",
+ |f, i| {
+ let hex = h32();
+ let ty = if i % 2 == 0 { "reserve" } else { "kyc" };
+ writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2)
+ },
+ )
+ .await;
+ b.table(
+ "talerable_outgoing_transactions(wtid, exchange_base_url, outgoing_transaction_id)",
+ |f, i| {
+ let hex = h32();
+ writeln!(f, "\\\\x{hex}\thttp://exchange.example.com/\t{}", i * 2 - 1)
+ },
+ )
+ .await;
+ b.table("transfer_operations(initiated_outgoing_transaction_id, request_uid, wtid, exchange_base_url)", |f, i| {
let h32 = h32();
let h64 = h64();
writeln!(f, "{i}\t\\\\x{h64}\t\\\\x{h32}\turl")
}).await;
- // Warm HTTP client
- server.get("/taler-revenue/config").await.assert_ok();
+ // Warm HTTP client
+ server.get("/taler-revenue/config").await.assert_ok();
- // Register
- b.measure("register_in", |_| malformed_in(&db)).await;
- b.measure("register_incomplete_in", |_| malformed_incomplete_in(&db))
- .await;
- b.measure("register_completed_in", |_| malformed_completeted_in(&db))
- .await;
- b.measure("register_talerable_in", |_| talerable_in(&db))
- .await;
- b.measure("register_talerable_incomplete_in", |_| {
- talerable_incomplete_in(&db)
- })
+ // Register
+ b.measure("register_in", |_| malformed_in(&db)).await;
+ b.measure("register_incomplete_in", |_| malformed_incomplete_in(&db))
.await;
- b.measure("register_talerable_completed_in", |_| {
- talerable_completeted_in(&db)
- })
+ b.measure("register_completed_in", |_| malformed_completeted_in(&db))
.await;
- b.measure("register_prepared_in", |_| prepared_in(&db))
- .await;
- b.measure("register_prepared_incomplete_in", |_| {
- prepared_incomplete_in(&db)
- })
+ b.measure("register_talerable_in", |_| talerable_in(&db))
.await;
- b.measure("register_prepared_completed_in", |_| {
- prepared_completeted_in(&db)
- })
+ b.measure("register_talerable_incomplete_in", |_| {
+ talerable_incomplete_in(&db)
+ })
+ .await;
+ b.measure("register_talerable_completed_in", |_| {
+ talerable_completeted_in(&db)
+ })
+ .await;
+ b.measure("register_prepared_in", |_| prepared_in(&db))
.await;
- b.measure("register_out", |_| malformed_out(&db)).await;
- b.measure("register_talerable_out", |_| talerable_out(&db))
- .await;
- b.measure("register_incomplete_out", |_| incomplete_out(&db))
- .await;
-
- // Revenue api
- b.measure("transaction_revenue", async |_| {
- server.get("/taler-revenue/history").await.assert_ok()
- })
+ b.measure("register_prepared_incomplete_in", |_| {
+ prepared_incomplete_in(&db)
+ })
+ .await;
+ b.measure("register_prepared_completed_in", |_| {
+ prepared_completeted_in(&db)
+ })
+ .await;
+ b.measure("register_out", |_| malformed_out(&db)).await;
+ b.measure("register_talerable_out", |_| talerable_out(&db))
.await;
-
- // Wire gateway
- b.measure("wg_transfer", async |_| {
- server
- .post("/taler-wire-gateway/transfer")
- .json(json!({
- "request_uid": HashCode::rand(),
- "amount": "KUDOS:0.0001",
- "exchange_base_url": "http://exchange.example.com/",
- "wtid": ShortHashCode::rand(),
- "credit_account": *CLIENT
- }))
- .await
- .assert_ok()
- })
- .await;
- b.measure("wg_transfer_get", async |i| {
- server
- .get(format!("/taler-wire-gateway/transfers/{}", i + 1))
- .await
- .assert_ok()
- })
- .await;
- b.measure("wg_transfer_page", async |_| {
- server
- .get("/taler-wire-gateway/transfers")
- .await
- .assert_ok()
- })
- .await;
- b.measure("wg_transfer_page_filter", async |_| {
- server
- .get("/taler-wire-gateway/transfers?status=success")
- .await
- .assert_no_content()
- })
- .await;
- b.measure("wg_add", async |_| {
- server
- .post("/taler-wire-gateway/admin/add-incoming")
- .json(json!({
- "amount": "KUDOS:0.0001",
- "reserve_pub": EddsaPublicKey::rand(),
- "debit_account": *CLIENT
- }))
- .await
- .assert_ok()
- })
- .await;
- b.measure("wg_incoming", async |_| {
- server
- .get("/taler-wire-gateway/history/incoming")
- .await
- .assert_ok()
- })
- .await;
- b.measure("wg_outgoing", async |_| {
- server
- .get("/taler-wire-gateway/history/outgoing")
- .await
- .assert_ok()
- })
+ b.measure("register_incomplete_out", |_| incomplete_out(&db))
.await;
- // Wire transfer
- b.measure("wt_register", async |i| {
- let (pair, key) = &accounts_pubs[i];
+ // Revenue api
+ b.measure("transaction_revenue", async |_| {
+ server.get("/taler-revenue/history").await.assert_ok()
+ })
+ .await;
- server
- .post("/taler-prepared-transfer/registration")
- .json(json!({
- "credit_account": *EXCHANGE,
- "credit_amount": "KUDOS:55",
- "type": "reserve",
- "alg": "EdDSA",
- "account_pub": key,
- "authorization_pub": key,
- "authorization_sig": eddsa_sign(pair, key.as_ref()),
- "recurrent":false
- }))
- .await
- .assert_ok()
- })
- .await;
- b.measure("wt_unregister", async |i| {
- let (pair, key) = &accounts_pubs[i];
- let now = Timestamp::now().to_string();
- let req = json!({
- "timestamp": &now,
+ // Wire gateway
+ b.measure("wg_transfer", async |_| {
+ server
+ .post("/taler-wire-gateway/transfer")
+ .json(json!({
+ "request_uid": HashCode::rand(),
+ "amount": "KUDOS:0.0001",
+ "exchange_base_url": "http://exchange.example.com/",
+ "wtid": ShortHashCode::rand(),
+ "credit_account": *CLIENT
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_transfer_get", async |i| {
+ server
+ .get(format!("/taler-wire-gateway/transfers/{}", i + 1))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_transfer_page", async |_| {
+ server
+ .get("/taler-wire-gateway/transfers")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_transfer_page_filter", async |_| {
+ server
+ .get("/taler-wire-gateway/transfers?status=success")
+ .await
+ .assert_no_content()
+ })
+ .await;
+ b.measure("wg_add", async |_| {
+ server
+ .post("/taler-wire-gateway/admin/add-incoming")
+ .json(json!({
+ "amount": "KUDOS:0.0001",
+ "reserve_pub": EddsaPublicKey::rand(),
+ "debit_account": *CLIENT
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_incoming", async |_| {
+ server
+ .get("/taler-wire-gateway/history/incoming")
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wg_outgoing", async |_| {
+ server
+ .get("/taler-wire-gateway/history/outgoing")
+ .await
+ .assert_ok()
+ })
+ .await;
+
+ // Wire transfer
+ b.measure("wt_register", async |i| {
+ let (pair, key) = &accounts_pubs[i];
+
+ server
+ .post("/taler-prepared-transfer/registration")
+ .json(json!({
+ "credit_account": *EXCHANGE,
+ "credit_amount": "KUDOS:55",
+ "type": "reserve",
+ "alg": "EdDSA",
+ "account_pub": key,
"authorization_pub": key,
- "authorization_sig": eddsa_sign(pair, now.as_ref()),
- });
- server
- .post("/taler-prepared-transfer/unregistration")
- .json(&req)
- .await
- .assert_no_content();
- server
- .post("/taler-prepared-transfer/unregistration")
- .json(&req)
- .await
- .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
- })
- .await;
- }
+ "authorization_sig": eddsa_sign(pair, key.as_ref()),
+ "recurrent":false
+ }))
+ .await
+ .assert_ok()
+ })
+ .await;
+ b.measure("wt_unregister", async |i| {
+ let (pair, key) = &accounts_pubs[i];
+ let now = Timestamp::now().to_string();
+ let req = json!({
+ "timestamp": &now,
+ "authorization_pub": key,
+ "authorization_sig": eddsa_sign(pair, now.as_ref()),
+ });
+ server
+ .post("/taler-prepared-transfer/unregistration")
+ .json(&req)
+ .await
+ .assert_no_content();
+ server
+ .post("/taler-prepared-transfer/unregistration")
+ .json(&req)
+ .await
+ .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
+ })
+ .await;
}
diff --git a/libeufin-nexus/src/lib.rs b/libeufin-nexus/src/lib.rs
@@ -66,6 +66,7 @@ use crate::{
};
pub mod api;
+#[cfg(test)]
pub mod bench;
pub mod config;
pub mod db;