bench.rs (20838B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 use std::fmt::Write as _; 21 22 use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _}; 23 use axum::http::header::AUTHORIZATION; 24 use compact_str::{CompactString, format_compact}; 25 use jiff::{Timestamp, Zoned, tz::TimeZone}; 26 use sqlx::postgres::PgConnectOptions; 27 use taler_common::{ 28 api::{ 29 EddsaPublicKey, EddsaSignature, HashCode, ShortHashCode, 30 prepared::{PublicKeyAlg, RegistrationRequest, TransferType, Unregistration}, 31 wire::TransferResponse, 32 }, 33 bench::{Bench, h16, h32, h64}, 34 encoding::hex, 35 error_code::ErrorCode, 36 types::{ 37 amount::{Amount, Currency}, 38 time::TalerTimestamp, 39 }, 40 }; 41 use taler_macros::db_test; 42 use taler_test_utils::{json, server::TestServer}; 43 use uuid::Uuid; 44 45 use crate::{ 46 api::{ 47 account::RegisterAccountResponse, 48 cashout::CashoutResponse, 49 conversion::ConversionRateClassResponse, 50 tan::ChallengeResponse, 51 test::{bank_setup, tan_code}, 52 token::TokenSuccessResponse, 53 tx::TransactionCreateResponse, 54 withdrawal::BankAccountCreateWithdrawalResponse, 55 }, 56 db::gc::collect, 57 pw::PwCrypto, 58 }; 59 60 #[db_test(raw)] 61 pub async fn bench_db(db: PgConnectOptions) { 62 let mut ctx = bank_setup(db).await; 63 let mut b = Bench::new(&ctx.state.db); 64 // Skip 4 accounts created by bank_setup 65 let skip = 4; 66 // Customer account will be used in tests so we want to generate more data for him 67 let customer = 3; 68 let exchange = 2; 69 // In general half of the data is for generated account and half is for customer 70 let mid = b.amount / 2; 71 72 let password = PwCrypto::Bcrypt { cost: 4 }.hashpw("password"); 73 let accounts_pubs: Vec<_> = (0..b.amount * 2) 74 .map(|_| { 75 let key_pair = Ed25519KeyPair::generate().unwrap(); 76 let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); 77 (key_pair, pub_key) 78 }) 79 .collect(); 80 81 b.table( 82 "customers(username, name, password_hash, cashout_payto)", 83 |f, i| { 84 writeln!( 85 f, 86 "account_{i}\t{password}\tMr n°{i}\t{}", 87 ctx.unknown_payto 88 ) 89 }, 90 ) 91 .await; 92 b.table("conversion_rate_classes(name)", |f, i| { 93 writeln!(f, "Class n°{i}") 94 }) 95 .await; 96 b.table( 97 "bank_accounts(internal_payto,owning_customer_id,is_public,conversion_rate_class_id)", 98 |f, i| { 99 let id = match i % 5 { 100 0 => "\\N", 101 1 | 2 => "1", 102 3 => "2", 103 _ => &(i % 10).to_string(), 104 }; 105 writeln!( 106 f, 107 "payto://x-taler-bank/localhost/account_{i}\t{}\t{}\t{id}", 108 i + skip, 109 i % 3 == 0 110 ) 111 }, 112 ) 113 .await; 114 b.table( 115 "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)", 116 |f, i| { 117 let account = if i > mid { customer} else {i+4}; 118 writeln!( 119 f, 120 "\\\\x{}\t0\t0\treadonly\tfalse\t{account}\t\\N\t0", 121 h32(), 122 ) 123 }, 124 ) 125 .await; 126 b.table( 127 "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)", 128 |f, i| { 129 let account = if i > mid { customer} else {i+4}; 130 writeln!( 131 f, 132 "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t{exchange}", 133 ctx.unknown_payto, ctx.unknown_payto 134 )?; 135 writeln!( 136 f, 137 "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t{account}", 138 ctx.unknown_payto, ctx.unknown_payto 139 ) 140 }, 141 ) 142 .await; 143 b.table("bank_transaction_operations", |f, i| { 144 writeln!(f, "\\\\x{}\t{i}", h32()) 145 }) 146 .await; 147 b.table("tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)", |f, i| { 148 let account = if i > mid { customer} else {i+4}; 149 writeln!(f, "{}\t\\\\x{}\t\\\\x{}\taccount_reconfig\tcode\t0\t0\t0\t{account}\tsms\tinfo", Uuid::new_v4(), h64(), h16()) 150 }) 151 .await; 152 b.table("taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)", |f, i| { 153 let account = if i > mid { customer} else {i+4}; 154 writeln!(f, "{}\t{account}\t\\\\x{}\t0", Uuid::new_v4(), h32()) 155 }) 156 .await; 157 b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)", |f, i| { 158 let ty = if i%2==0 {"reserve"} else {"kyc"}; 159 let recurrent = if i%3 == 0 {"true" } else {"false"}; 160 let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)}; 161 162 let key = hex::encode( accounts_pubs[i].1.as_ref()); 163 let sig = h64(); 164 writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t0\t{incoming_transaction_id}") 165 }).await; 166 b.table( 167 "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)", 168 |f, i| { 169 let key = hex::encode(accounts_pubs[i].1.as_ref()); 170 writeln!(f, "{}\t{i}\t\\\\x{key}", i * 2) 171 }, 172 ) 173 .await; 174 b.table("taler_exchange_outgoing(bank_transaction)", |f, i| { 175 writeln!(f, "{}", i * 2 - 1) 176 }) 177 .await; 178 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| { 179 if i % 2 == 0 { 180 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()) 181 } else { 182 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()) 183 } 184 }) 185 .await; 186 b.table( 187 "taler_exchange_incoming(type, metadata, bank_transaction)", 188 |f, i| { 189 let hex = h32(); 190 let ty = if i % 2 == 0 { "reserve" } else { "kyc" }; 191 writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2) 192 }, 193 ) 194 .await; 195 b.table("bank_stats(timeframe, start_time)", |f, i| { 196 writeln!( 197 f, 198 "day\t{}", 199 Zoned::new(Timestamp::from_second(i as i64).unwrap(), TimeZone::UTC).datetime() 200 ) 201 }) 202 .await; 203 b.table("cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)", |f, i| { 204 let account = if i > mid { customer} else {i+4}; 205 writeln!(f, "\\\\x{}\t(0,0)\t(0,0)\tsubject\t0\t{account}\t{i}", h32()) 206 }) 207 .await; 208 209 // Prepare customer accounts 210 ctx.fill_cashout_info("customer").await; 211 ctx.set_max_debt("customer", "1000000").await; 212 213 // Accounts 214 let mut names = Vec::new(); 215 b.measure("account_create", async |i| { 216 let username = format!("account_bench_{i}"); 217 ctx.post("/accounts") 218 .json(json!({ 219 "username": username, 220 "password": format!("account_bench_{i}-password"), 221 "name": format!("Bench Account {i}") 222 })) 223 .await 224 .assert_ok_json::<RegisterAccountResponse>(); 225 names.push(username); 226 }) 227 .await; 228 ctx.client 229 .cache_tokens(&names.iter().map(|it| it.as_str()).collect::<Vec<_>>()) 230 .await; 231 b.measure("account_reconfig", async |i| { 232 ctx.patcha(format!("/accounts/account_bench_{i}")) 233 .json(json!({ 234 "name": format!("New Bench Account {i}") 235 })) 236 .await 237 .assert_no_content(); 238 }) 239 .await; 240 b.measure("account_reconfig_auth", async |i| { 241 ctx.patcha(format!("/accounts/account_bench_{i}/auth")) 242 .json(json!({ 243 "old_password": format!("account_bench_{i}-password"), 244 "new_password": format!("account_bench_{i}-password"), 245 })) 246 .await 247 .assert_no_content(); 248 }) 249 .await; 250 b.measure("account_list", async |_| { 251 ctx.get_admin("/accounts").await.assert_ok() 252 }) 253 .await; 254 b.measure("account_list_class", async |i| { 255 ctx.get_admin(format!("/accounts?conversion_rate_class={}", i % 10)) 256 .await 257 .assert_ok() 258 }) 259 .await; 260 b.measure("account_list_name", async |_| { 261 ctx.get_admin("/accounts?name=Mr").await.assert_ok() 262 }) 263 .await; 264 b.measure("account_list_public", async |_| { 265 ctx.get("/public-accounts").await.assert_ok() 266 }) 267 .await; 268 b.measure("account_get", async |i| { 269 ctx.geta(format!("/accounts/account_bench_{i}")) 270 .await 271 .assert_ok() 272 }) 273 .await; 274 275 // Token 276 let mut t = Vec::new(); 277 b.measure("token_create", async |_| { 278 t.push( 279 ctx.postpw("/accounts/customer/token") 280 .json(json!({ 281 "scope": "readonly", 282 "refreshable": true 283 })) 284 .await 285 .assert_ok_json::<TokenSuccessResponse>() 286 .access_token, 287 ) 288 }) 289 .await; 290 b.measure("token_refresh", async |i| { 291 ctx.post("/accounts/customer/token") 292 .header(AUTHORIZATION, format!("Bearer {}", t[i])) 293 .json(json!({ 294 "scope": "readonly" 295 })) 296 .await 297 .assert_ok() 298 }) 299 .await; 300 b.measure("token_list", async |_| { 301 ctx.geta("/accounts/customer/tokens").await.assert_ok() 302 }) 303 .await; 304 b.measure("token_delete", async |i| { 305 ctx.delete("/accounts/customer/token") 306 .header(AUTHORIZATION, format!("Bearer {}", t[i])) 307 .await 308 .assert_no_content(); 309 }) 310 .await; 311 312 // Conversion rate classes 313 let mut c = Vec::new(); 314 b.measure("class_create", async |i| { 315 c.push( 316 ctx.post_admin("/conversion-rate-classes") 317 .json(json!({ 318 "name": format!("Gen class {i}") 319 })) 320 .await 321 .assert_ok_json::<ConversionRateClassResponse>() 322 .conversion_rate_class_id, 323 ) 324 }) 325 .await; 326 b.measure("class_patch", async |i| { 327 ctx.patch_admin(format!("/conversion-rate-classes/{}", c[i])) 328 .json(json!({ 329 "name": format!("Gen class {i}"), 330 "description": format!("test {i}") 331 })) 332 .await 333 .assert_no_content() 334 }) 335 .await; 336 b.measure("class_get", async |i| { 337 ctx.get_admin(format!("/conversion-rate-classes/{}", c[i])) 338 .await 339 .assert_ok() 340 }) 341 .await; 342 b.measure("class_list", async |_| { 343 ctx.get_admin("/conversion-rate-classes").await.assert_ok() 344 }) 345 .await; 346 b.measure("class_delete", async |i| { 347 ctx.delete_admin(format!("/conversion-rate-classes/{}", c[i])) 348 .await 349 .assert_no_content() 350 }) 351 .await; 352 353 // Transaction 354 let mut tx = Vec::new(); 355 b.measure("transaction_create", async |_| { 356 tx.push( 357 ctx.posta("/accounts/customer/transactions") 358 .json(json!({ 359 "payto_uri": format!("{}?receiver-name=Test&message=payout", ctx.merchant_payto), 360 "amount": "KUDOS:0.001" 361 })) 362 .await 363 .assert_ok_json::<TransactionCreateResponse>() 364 .row_id, 365 ) 366 }) 367 .await; 368 b.measure("transaction_get", async |i| { 369 ctx.geta(format!("/accounts/customer/transactions/{}", tx[i])) 370 .await 371 .assert_ok() 372 }) 373 .await; 374 b.measure("transaction_history", async |_| { 375 ctx.geta("/accounts/customer/transactions") 376 .await 377 .assert_ok() 378 }) 379 .await; 380 b.measure("transaction_revenue", async |_| { 381 ctx.geta("/accounts/merchant/taler-revenue/history") 382 .await 383 .assert_ok() 384 }) 385 .await; 386 387 // Withdrawal 388 let mut w = Vec::new(); 389 b.measure("withdrawal_create", async |_| { 390 w.push( 391 ctx.posta("/accounts/customer/withdrawals") 392 .json(json!({ 393 "amount": "KUDOS:0.0001" 394 })) 395 .await 396 .assert_ok_json::<BankAccountCreateWithdrawalResponse>() 397 .withdrawal_id, 398 ) 399 }) 400 .await; 401 b.measure("withdrawal_get", async |i| { 402 ctx.get(format!("/withdrawals/{}", w[i])).await.assert_ok() 403 }) 404 .await; 405 b.measure("withdrawal_status", async |i| { 406 ctx.get(format!("/taler-integration/withdrawal-operation/{}", w[i])) 407 .await 408 .assert_ok() 409 }) 410 .await; 411 b.measure("withdrawal_select", async |i| { 412 ctx.post(format!("/taler-integration/withdrawal-operation/{}", w[i])) 413 .json(json!({ 414 "reserve_pub": &EddsaPublicKey::rand(), 415 "selected_exchange": &ctx.exchange_payto 416 })) 417 .await 418 .assert_ok() 419 }) 420 .await; 421 b.measure("withdrawal_confirm", async |i| { 422 ctx.posta(format!("/accounts/customer/withdrawals/{}/confirm", w[i])) 423 .json(json!({})) 424 .await 425 .assert_no_content() 426 }) 427 .await; 428 b.measure("withdrawal_abort", async |i| { 429 ctx.posta(format!("/accounts/customer/withdrawals/{}/abort", w[i])) 430 .await 431 .assert_error(ErrorCode::BANK_ABORT_CONFIRM_CONFLICT); 432 }) 433 .await; 434 435 // Cashout 436 let mut c = Vec::new(); 437 b.measure("cashout_create", async |_| { 438 c.push( 439 ctx.posta("/accounts/customer/cashouts") 440 .json(json!({ 441 "request_uid": ShortHashCode::rand(), 442 "amount_debit": "KUDOS:0.1", 443 "amount_credit": &ctx.convert("KUDOS:0.1").await 444 })) 445 .await 446 .assert_ok_json::<CashoutResponse>() 447 .cashout_id, 448 ) 449 }) 450 .await; 451 b.measure("cashout_get", async |i| { 452 ctx.geta(format!("/accounts/customer/cashouts/{}", c[i])) 453 .await 454 .assert_ok() 455 }) 456 .await; 457 b.measure("cashout_history", async |_| { 458 ctx.geta("/accounts/customer/cashouts").await.assert_ok() 459 }) 460 .await; 461 b.measure("cashout_history_admin", async |_| { 462 ctx.get_admin("/cashouts").await.assert_ok() 463 }) 464 .await; 465 466 // Wire gateway 467 let mut t = Vec::new(); 468 b.measure("wg_transfer", async |_| { 469 t.push( 470 ctx.posta("/accounts/exchange/taler-wire-gateway/transfer") 471 .json(json!({ 472 "request_uid": HashCode::rand(), 473 "amount": "KUDOS:0.0001", 474 "exchange_base_url": "http://exchange.example.com/", 475 "wtid": ShortHashCode::rand(), 476 "credit_account": &ctx.customer_payto 477 })) 478 .await 479 .assert_ok_json::<TransferResponse>() 480 .row_id, 481 ) 482 }) 483 .await; 484 b.measure("wg_transfer_get", async |i| { 485 ctx.geta(format!( 486 "/accounts/exchange/taler-wire-gateway/transfers/{}", 487 t[i] 488 )) 489 .await 490 .assert_ok() 491 }) 492 .await; 493 b.measure("wg_transfer_page", async |_| { 494 ctx.geta("/accounts/exchange/taler-wire-gateway/transfers") 495 .await 496 .assert_ok() 497 }) 498 .await; 499 b.measure("wg_transfer_page_filter", async |_| { 500 ctx.geta("/accounts/exchange/taler-wire-gateway/transfers?status=success") 501 .await 502 .assert_ok() 503 }) 504 .await; 505 b.measure("wg_add", async |_| { 506 ctx.posta("/accounts/exchange/taler-wire-gateway/admin/add-incoming") 507 .json(json!({ 508 "amount": "KUDOS:0.0001", 509 "reserve_pub": EddsaPublicKey::rand(), 510 "debit_account": &ctx.customer_payto 511 })) 512 .await 513 .assert_ok() 514 }) 515 .await; 516 b.measure("wg_incoming", async |_| { 517 ctx.geta("/accounts/exchange/taler-wire-gateway/history/incoming") 518 .await 519 .assert_ok() 520 }) 521 .await; 522 b.measure("wg_outgoing", async |_| { 523 ctx.geta("/accounts/exchange/taler-wire-gateway/history/outgoing") 524 .await 525 .assert_ok() 526 }) 527 .await; 528 529 // TAN challenges 530 let mut c = Vec::new(); 531 b.measure("tan_send", async |i| { 532 let res = ctx 533 .patcha(format!("/accounts/account_bench_{i}")) 534 .json(json!({ 535 "contact_data" : { 536 "phone": "+99", 537 "email": "email@example.com" 538 }, 539 "tan_channel": "sms" 540 })) 541 .await 542 .assert_accepted_json::<ChallengeResponse>(); 543 let challenge = &res.challenges[0]; 544 ctx.posta(format!( 545 "/accounts/account_bench_{i}/challenge/{}", 546 challenge.challenge_id 547 )) 548 .await 549 .assert_ok(); 550 let code = tan_code(&challenge.tan_info); 551 c.push((challenge.challenge_id.clone(), code)) 552 }) 553 .await; 554 b.measure("tan_confirm", async |i| { 555 let (id, code) = &c[i]; 556 ctx.posta(format!( 557 "/accounts/account_bench_{i}/challenge/{id}/confirm" 558 )) 559 .json(json!({ 560 "tan": code 561 })) 562 .await 563 .assert_no_content(); 564 }) 565 .await; 566 567 // Revenue api 568 b.measure("transaction_revenue", async |_| { 569 ctx.geta("/accounts/exchange/taler-revenue/history") 570 .await 571 .assert_ok() 572 }) 573 .await; 574 575 // Wire transfer 576 b.measure("wt_register", async |i| { 577 let (pair, key) = &accounts_pubs[i]; 578 let req = RegistrationRequest { 579 credit_account: ctx.exchange_payto.as_uri(), 580 r#type: TransferType::reserve, 581 recurrent: false, 582 credit_amount: Amount::new(&Currency::KUDOS, 55, 0), 583 alg: PublicKeyAlg::EdDSA, 584 account_pub: *key, 585 authorization_pub: *key, 586 authorization_sig: EddsaSignature::ZEROED, 587 } 588 .signed(pair); 589 for _ in 0..2 { 590 ctx.post("/taler-prepared-transfer/registration") 591 .json(&req) 592 .await 593 .assert_ok() 594 } 595 }) 596 .await; 597 b.measure("wt_unregister", async |i| { 598 let (pair, key) = &accounts_pubs[i]; 599 let req = Unregistration { 600 timestamp: TalerTimestamp::Timestamp(Timestamp::now()), 601 authorization_pub: *key, 602 authorization_sig: EddsaSignature::ZEROED, 603 } 604 .signed(pair); 605 ctx.post("/taler-prepared-transfer/unregistration") 606 .json(&req) 607 .await 608 .assert_no_content(); 609 ctx.post("/taler-prepared-transfer/unregistration") 610 .json(&req) 611 .await 612 .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); 613 }) 614 .await; 615 616 // Delete accounts 617 b.measure("account_delete", async |i| { 618 ctx.deletea(format!("/accounts/account_bench_{i}")) 619 .await 620 .assert_no_content() 621 }) 622 .await; 623 624 // Other 625 b.measure("monitor", async |_| { 626 ctx.get_admin("/monitor").await.assert_ok(); 627 }) 628 .await; 629 let now = Timestamp::MIN; 630 collect(&ctx.state.db, &now, &now, &now).await.unwrap(); 631 b.measure("gc", async |_| { 632 collect(&ctx.state.db, &now, &now, &now).await.unwrap(); 633 }) 634 .await; 635 }