libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 46e8184ce90eeeeb2a4f6e15a5116de977e67d4a
parent 4d3a511087635e2b4d8389c861f1d3b75f158f78
Author: Antoine A <>
Date:   Sat, 13 Jun 2026 11:45:25 +0200

nexus: add bench

Diffstat:
MCargo.lock | 6++++++
MCargo.toml | 26+++++++++++++-------------
MMakefile | 13++++++++++---
Mlibeufin-bank/src/api.rs | 2+-
Alibeufin-bank/src/bench.rs | 624+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlibeufin-bank/src/lib.rs | 2++
Mlibeufin-nexus/src/bench.rs | 491+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mlibeufin-nexus/src/lib.rs | 1+
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;