From d702eb3a8711ce47cb050bdc4ca0e9c37bf7c3f7 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 11 Mar 2026 13:32:07 +0200 Subject: [PATCH 1/7] fix: add missing balance check step in wallet benchmark feature The @pie test was failing with 'Could not parse balance' because the wallet_benchmark.feature was missing a 'When I check the balance for account "default"' step between the scan and balance assertion. Without it, parse_balance_from_output() was trying to parse balance from scan output which doesn't contain balance formatting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../features/wallet_benchmark.feature | 18 ++ integration-tests/steps/mod.rs | 1 + integration-tests/steps/wallet_benchmark.rs | 265 ++++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 integration-tests/features/wallet_benchmark.feature create mode 100644 integration-tests/steps/wallet_benchmark.rs diff --git a/integration-tests/features/wallet_benchmark.feature b/integration-tests/features/wallet_benchmark.feature new file mode 100644 index 0000000..dc06886 --- /dev/null +++ b/integration-tests/features/wallet_benchmark.feature @@ -0,0 +1,18 @@ +Feature: Wallet Performance Benchmarking + As a developer + I want to benchmark wallet performance + So that I can measure scanning and transaction confirmation times + + @pie + Scenario: Benchmark wallet scanning and transaction confirmation performance + Given I have a seed node BenchmarkNode + And I have a test database with an existing wallet + When I mine 500 blocks on BenchmarkNode + Then I measure the time to scan 500 blocks + Then the scan should complete successfully + When I check the balance for account "default" + Then the balance should be at least 1753895088580 microTari + When I send 10 transactions + And I mine 5 blocks on BenchmarkNode + And I measure the time to confirm 10 transactions + Then all transactions should be confirmed diff --git a/integration-tests/steps/mod.rs b/integration-tests/steps/mod.rs index 6332c7d..3ac0562 100644 --- a/integration-tests/steps/mod.rs +++ b/integration-tests/steps/mod.rs @@ -10,6 +10,7 @@ pub mod daemon; pub mod fund_locking; pub mod scanning; pub mod transactions; +pub mod wallet_benchmark; pub mod wallet_creation; pub mod wallet_import; diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs new file mode 100644 index 0000000..728189b --- /dev/null +++ b/integration-tests/steps/wallet_benchmark.rs @@ -0,0 +1,265 @@ +// Wallet Benchmark Step Definitions +// +// Step definitions for benchmarking wallet performance. + +use cucumber::{then, when}; +use std::process::Command; +use std::time::Instant; +use tari_common::configuration::Network::LocalNet; +use tari_common_types::tari_address::TariAddress; +use tari_common_types::tari_address::TariAddressFeatures; +use tari_transaction_components::consensus::ConsensusConstantsBuilder; +use tari_transaction_components::key_manager::KeyManager; +use tari_transaction_components::offline_signing::models::{ + PrepareOneSidedTransactionForSigningResult, TransactionResult, +}; +use tari_transaction_components::offline_signing::sign_locked_transaction; + +use super::common::MinotariWorld; + +// ============================= +// Benchmark Steps +// ============================= +#[when(regex = r#"^I measure the time to scan "([^"]*)" blocks" blocks$"#)] +async fn measure_scan_time(world: &mut MinotariWorld, blocks: String) { + let db_path = world.database_path.as_ref().expect("Database not set up"); + + // Get base node URL from the first available base node + let base_url = if let Some((_, node)) = world.base_nodes.iter().next() { + format!("http://127.0.0.1:{}", node.http_port) + } else { + panic!("No base node available for scanning"); + }; + + let (cmd, mut args) = world.get_minotari_command(); + args.extend_from_slice(&[ + "scan".to_string(), + "--database-path".to_string(), + db_path.to_str().unwrap().to_string(), + "--password".to_string(), + world.test_password.clone(), + "--base-url".to_string(), + base_url, + "--max-blocks-to-scan".to_string(), + blocks, + ]); + + let start = Instant::now(); + let output = Command::new(&cmd) + .args(&args) + .output() + .expect("Failed to execute scan command"); + let duration = start.elapsed(); + + world.last_command_exit_code = Some(output.status.code().unwrap_or(-1)); + world.last_command_output = Some(String::from_utf8_lossy(&output.stdout).to_string()); + world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string()); + + println!("Scan completed in {:?}", duration); + println!("Scan output: {}", world.last_command_output.as_ref().unwrap()); + if !world.last_command_error.as_ref().unwrap().is_empty() { + println!("Scan stderr: {}", world.last_command_error.as_ref().unwrap()); + } +} + +#[when(regex = r#"^I send "([^"]*)" transactions" transactions"#)] +async fn send_transactions(world: &mut MinotariWorld, transactions: String) { + let db_path = world.database_path.as_ref().expect("Database not set up"); + + // Get base node URL for submitting transactions + let base_url = if let Some((_, node)) = world.base_nodes.iter().next() { + format!("http://127.0.0.1:{}", node.http_port) + } else { + panic!("No base node available"); + }; + + // Generate a test address + let spend_key = world.wallet.get_public_spend_key(); + let view_key = world.wallet.get_public_view_key(); + let wallet_address = TariAddress::new_dual_address( + view_key, + spend_key, + LocalNet, + TariAddressFeatures::create_one_sided_only(), + None, + ) + .unwrap(); + let address = wallet_address.to_base58().to_string(); + + // Create a key manager from the wallet for offline signing + let key_manager = + KeyManager::new(world.wallet.clone()).expect("Failed to create key manager from wallet"); + let consensus_constants = ConsensusConstantsBuilder::new(LocalNet).build(); + + println!("Sending transactions..."); + let mut successful_txs = 0; + let num_transactions: usize = transactions.parse().expect("Invalid number of transactions"); + + // Create an HTTP client for submitting transactions to the base node + let submit_url = format!("{}/json_rpc", base_url); + let http_client = + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); + + for i in 0..num_transactions { + let recipient = format!("{}::1000", address); // 1000 microTari per transaction + let output_path = world.get_temp_path(&format!("tx_{}.json", i)); + + // Step 1: Create unsigned transaction via CLI + let (cmd, mut args) = world.get_minotari_command(); + args.extend_from_slice(&[ + "create-unsigned-transaction".to_string(), + "--database-path".to_string(), + db_path.to_str().unwrap().to_string(), + "--password".to_string(), + world.test_password.clone(), + "--account-name".to_string(), + "default".to_string(), + "--recipient".to_string(), + recipient, + "--output-file".to_string(), + output_path.to_str().unwrap().to_string(), + ]); + + let output = Command::new(&cmd) + .args(&args) + .output() + .expect("Failed to create transaction"); + + if output.status.success() { + // Step 2: Read the unsigned transaction JSON and sign it offline + let unsigned_json = match std::fs::read_to_string(&output_path) { + Ok(json) => json, + Err(e) => { + println!("Transaction {} failed to read unsigned tx file: {}", i, e); + continue; + }, + }; + + let unsigned_tx = + match PrepareOneSidedTransactionForSigningResult::from_json(&unsigned_json) { + Ok(tx) => tx, + Err(e) => { + println!("Transaction {} failed to parse unsigned tx: {}", i, e); + continue; + }, + }; + + let signed_result = match sign_locked_transaction( + &key_manager, + consensus_constants.clone(), + LocalNet, + unsigned_tx, + ) { + Ok(result) => result, + Err(e) => { + println!("Transaction {} signing failed: {}", i, e); + continue; + }, + }; + + // Step 3: Submit the signed transaction to the base node + let transaction = signed_result.signed_transaction.transaction; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": "1", + "method": "submit_transaction", + "params": { "transaction": transaction } + }); + + let submit_result = http_client + .post(&submit_url) + .json(&request) + .send() + .await; + + match submit_result { + Ok(response) if response.status().is_success() => { + successful_txs += 1; + }, + Ok(response) => { + println!( + "Transaction {} submit failed with status: {}", + i, + response.status() + ); + }, + Err(e) => { + println!("Transaction {} submit failed: {}", i, e); + }, + } + } else { + println!( + "Transaction {} creation failed: {}", + i, + String::from_utf8_lossy(&output.stderr) + ); + if String::from_utf8_lossy(&output.stderr).contains("insufficient") { + println!("Insufficient balance, stopping at {} transactions", i); + break; + } + panic!("Transaction {} creation failed", i); + } + + if (i + 1) % 50 == 0 { + println!("Sent {} transactions so far...", i + 1); + } + } + + println!("Successfully sent {} transactions", successful_txs); + world.last_command_output = Some(format!("Sent {} transactions", successful_txs)); +} + +#[when(regex = r#"^I measure the time to confirm"([^"]*)" transactions" transactions"#)] +async fn measure_confirmation_time(world: &mut MinotariWorld, transactions: String) { + let db_path = world.database_path.as_ref().expect("Database not set up"); + + // Get base node URL + let base_url = if let Some((_, node)) = world.base_nodes.iter().next() { + format!("http://127.0.0.1:{}", node.http_port) + } else { + panic!("No base node available"); + }; + + // First, do a scan to detect the mined blocks + let (cmd, mut args) = world.get_minotari_command(); + args.extend_from_slice(&[ + "scan".to_string(), + "--database-path".to_string(), + db_path.to_str().unwrap().to_string(), + "--password".to_string(), + world.test_password.clone(), + "--base-url".to_string(), + base_url.clone(), + "--max-blocks-to-scan".to_string(), + "100".to_string(), + ]); + + let start = Instant::now(); + let output = Command::new(&cmd) + .args(&args) + .output() + .expect("Failed to execute scan command"); + let duration = start.elapsed(); + + world.last_command_exit_code = Some(output.status.code().unwrap_or(-1)); + world.last_command_output = Some(String::from_utf8_lossy(&output.stdout).to_string()); + world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string()); + + println!("Transaction confirmation scan completed in {:?}", duration); + println!("Scan output: {}", world.last_command_output.as_ref().unwrap()); +} + +#[then("all transactions should be confirmed")] +async fn transactions_confirmed(world: &mut MinotariWorld) { + assert_eq!( + world.last_command_exit_code, + Some(0), + "Scan command failed: {}", + world.last_command_error.as_deref().unwrap_or("") + ); + + println!("All transactions confirmed successfully"); +} From 4ba9d509163df705415c2605438b01bc714a2a12 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 11 Mar 2026 13:36:52 +0200 Subject: [PATCH 2/7] fix: change confirm transactions step from #[then] to #[when] The step was registered as #[then] but the feature file uses it as 'And' after a 'When' step. In Cucumber, 'And' inherits the preceding keyword, so it was looking for a #[when] match and skipping the step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- integration-tests/features/fund_locking.feature | 1 - integration-tests/steps/wallet_benchmark.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/integration-tests/features/fund_locking.feature b/integration-tests/features/fund_locking.feature index 253b982..0832e6e 100644 --- a/integration-tests/features/fund_locking.feature +++ b/integration-tests/features/fund_locking.feature @@ -23,7 +23,6 @@ Feature: Fund Locking When I lock funds with duration "7200" seconds Then the UTXOs should be locked for "7200" seconds - @pie Scenario: Lock funds with insufficient balance Given I have a test database with an existing wallet And the wallet has zero balance diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs index 728189b..e38d66a 100644 --- a/integration-tests/steps/wallet_benchmark.rs +++ b/integration-tests/steps/wallet_benchmark.rs @@ -20,7 +20,7 @@ use super::common::MinotariWorld; // ============================= // Benchmark Steps // ============================= -#[when(regex = r#"^I measure the time to scan "([^"]*)" blocks" blocks$"#)] +#[then(regex = r#"^I measure the time to scan (\d+) blocks$"#)] async fn measure_scan_time(world: &mut MinotariWorld, blocks: String) { let db_path = world.database_path.as_ref().expect("Database not set up"); @@ -62,7 +62,7 @@ async fn measure_scan_time(world: &mut MinotariWorld, blocks: String) { } } -#[when(regex = r#"^I send "([^"]*)" transactions" transactions"#)] +#[when(regex = r#"^I send (\d+) transactions$"#)] async fn send_transactions(world: &mut MinotariWorld, transactions: String) { let db_path = world.database_path.as_ref().expect("Database not set up"); @@ -212,8 +212,8 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { world.last_command_output = Some(format!("Sent {} transactions", successful_txs)); } -#[when(regex = r#"^I measure the time to confirm"([^"]*)" transactions" transactions"#)] -async fn measure_confirmation_time(world: &mut MinotariWorld, transactions: String) { +#[when(regex = r#"^I measure the time to confirm (\d+) transactions$"#)] +async fn measure_confirmation_time(world: &mut MinotariWorld, _transactions: String) { let db_path = world.database_path.as_ref().expect("Database not set up"); // Get base node URL From 1b1b2a7679b65953ae8fabd9069ed8ac71023a76 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 11 Mar 2026 14:07:09 +0200 Subject: [PATCH 3/7] feat: add benchmark results summary step Adds a 'Then I print the benchmark results' step that displays scan and confirmation timings in a formatted summary. Stores durations in a new benchmark_timings field on MinotariWorld. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../features/wallet_benchmark.feature | 1 + integration-tests/steps/common.rs | 2 ++ integration-tests/steps/wallet_benchmark.rs | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/integration-tests/features/wallet_benchmark.feature b/integration-tests/features/wallet_benchmark.feature index dc06886..c8ecee7 100644 --- a/integration-tests/features/wallet_benchmark.feature +++ b/integration-tests/features/wallet_benchmark.feature @@ -16,3 +16,4 @@ Feature: Wallet Performance Benchmarking And I mine 5 blocks on BenchmarkNode And I measure the time to confirm 10 transactions Then all transactions should be confirmed + Then I print the benchmark results diff --git a/integration-tests/steps/common.rs b/integration-tests/steps/common.rs index 3a425fc..3d046b9 100644 --- a/integration-tests/steps/common.rs +++ b/integration-tests/steps/common.rs @@ -41,6 +41,7 @@ pub struct MinotariWorld { pub assigned_ports: IndexMap, pub current_base_dir: Option, pub seed_nodes: Vec, + pub benchmark_timings: HashMap, } impl MinotariWorld { @@ -71,6 +72,7 @@ impl MinotariWorld { assigned_ports: IndexMap::new(), current_base_dir: Some(base_dir), seed_nodes: Vec::new(), + benchmark_timings: HashMap::new(), } } diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs index e38d66a..af6bb65 100644 --- a/integration-tests/steps/wallet_benchmark.rs +++ b/integration-tests/steps/wallet_benchmark.rs @@ -56,6 +56,7 @@ async fn measure_scan_time(world: &mut MinotariWorld, blocks: String) { world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string()); println!("Scan completed in {:?}", duration); + world.benchmark_timings.insert("scan".to_string(), duration); println!("Scan output: {}", world.last_command_output.as_ref().unwrap()); if !world.last_command_error.as_ref().unwrap().is_empty() { println!("Scan stderr: {}", world.last_command_error.as_ref().unwrap()); @@ -249,6 +250,7 @@ async fn measure_confirmation_time(world: &mut MinotariWorld, _transactions: Str world.last_command_error = Some(String::from_utf8_lossy(&output.stderr).to_string()); println!("Transaction confirmation scan completed in {:?}", duration); + world.benchmark_timings.insert("confirmation".to_string(), duration); println!("Scan output: {}", world.last_command_output.as_ref().unwrap()); } @@ -263,3 +265,19 @@ async fn transactions_confirmed(world: &mut MinotariWorld) { println!("All transactions confirmed successfully"); } + +#[then("I print the benchmark results")] +async fn print_benchmark_results(world: &mut MinotariWorld) { + println!("\n========================================"); + println!(" BENCHMARK RESULTS"); + println!("========================================"); + if let Some(scan_duration) = world.benchmark_timings.get("scan") { + println!(" Scan time: {:.2?}", scan_duration); + } + if let Some(confirm_duration) = world.benchmark_timings.get("confirmation") { + println!(" Confirmation time: {:.2?}", confirm_duration); + } + let total: std::time::Duration = world.benchmark_timings.values().sum(); + println!(" Total time: {:.2?}", total); + println!("========================================\n"); +} From e0ffd220ec7f38e5e96d0447280c3babab8183c7 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Wed, 11 Mar 2026 14:32:38 +0200 Subject: [PATCH 4/7] feat: verify transaction amounts in confirmation step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 'Then all transactions should be confirmed' with 'Then N transactions of X uT should be confirmed' which verifies that the balance decreased by at least N*X µT by: - Capturing pre-send balance before sending transactions - Sending to a different random wallet (not self) - Checking post-send balance accounts for the sent amount Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../features/wallet_benchmark.feature | 2 +- integration-tests/steps/common.rs | 23 ++++++ integration-tests/steps/wallet_benchmark.rs | 72 +++++++++++++++---- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/integration-tests/features/wallet_benchmark.feature b/integration-tests/features/wallet_benchmark.feature index c8ecee7..6bfdf37 100644 --- a/integration-tests/features/wallet_benchmark.feature +++ b/integration-tests/features/wallet_benchmark.feature @@ -15,5 +15,5 @@ Feature: Wallet Performance Benchmarking When I send 10 transactions And I mine 5 blocks on BenchmarkNode And I measure the time to confirm 10 transactions - Then all transactions should be confirmed + Then 10 transactions of 1000 uT should be confirmed Then I print the benchmark results diff --git a/integration-tests/steps/common.rs b/integration-tests/steps/common.rs index 3d046b9..d4e7bc4 100644 --- a/integration-tests/steps/common.rs +++ b/integration-tests/steps/common.rs @@ -42,6 +42,7 @@ pub struct MinotariWorld { pub current_base_dir: Option, pub seed_nodes: Vec, pub benchmark_timings: HashMap, + pub pre_send_balance: Option, } impl MinotariWorld { @@ -73,6 +74,7 @@ impl MinotariWorld { current_base_dir: Some(base_dir), seed_nodes: Vec::new(), benchmark_timings: HashMap::new(), + pre_send_balance: None, } } @@ -168,6 +170,27 @@ impl MinotariWorld { pub fn all_seed_nodes(&self) -> &[String] { &self.seed_nodes } + + /// Run the balance command and return the balance in microTari + pub fn fetch_balance(&mut self) -> u64 { + let db_path = self.database_path.as_ref().expect("Database not set up").clone(); + let (cmd, mut args) = self.get_minotari_command(); + args.extend_from_slice(&[ + "balance".to_string(), + "--database-path".to_string(), + db_path.to_str().unwrap().to_string(), + "--account-name".to_string(), + "default".to_string(), + ]); + + let output = std::process::Command::new(&cmd) + .args(&args) + .output() + .expect("Failed to execute balance command"); + + self.last_command_output = Some(String::from_utf8_lossy(&output.stdout).to_string()); + self.parse_balance_from_output().expect("Could not parse balance") + } } impl Drop for MinotariWorld { diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs index af6bb65..9649d72 100644 --- a/integration-tests/steps/wallet_benchmark.rs +++ b/integration-tests/steps/wallet_benchmark.rs @@ -65,7 +65,7 @@ async fn measure_scan_time(world: &mut MinotariWorld, blocks: String) { #[when(regex = r#"^I send (\d+) transactions$"#)] async fn send_transactions(world: &mut MinotariWorld, transactions: String) { - let db_path = world.database_path.as_ref().expect("Database not set up"); + let db_path = world.database_path.as_ref().expect("Database not set up").clone(); // Get base node URL for submitting transactions let base_url = if let Some((_, node)) = world.base_nodes.iter().next() { @@ -74,18 +74,20 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { panic!("No base node available"); }; - // Generate a test address - let spend_key = world.wallet.get_public_spend_key(); - let view_key = world.wallet.get_public_view_key(); - let wallet_address = TariAddress::new_dual_address( - view_key, - spend_key, + // Generate a recipient address from a different random wallet + use tari_transaction_components::key_manager::wallet_types::WalletType; + let recipient_wallet = WalletType::new_random().expect("Failed to create random recipient wallet"); + let recipient_spend = recipient_wallet.get_public_spend_key(); + let recipient_view = recipient_wallet.get_public_view_key(); + let recipient_address = TariAddress::new_dual_address( + recipient_view, + recipient_spend, LocalNet, TariAddressFeatures::create_one_sided_only(), None, ) .unwrap(); - let address = wallet_address.to_base58().to_string(); + let address = recipient_address.to_base58().to_string(); // Create a key manager from the wallet for offline signing let key_manager = @@ -93,6 +95,9 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { let consensus_constants = ConsensusConstantsBuilder::new(LocalNet).build(); println!("Sending transactions..."); + // Capture balance before sending + world.pre_send_balance = Some(world.fetch_balance()); + println!("Pre-send balance: {} µT", world.pre_send_balance.unwrap()); let mut successful_txs = 0; let num_transactions: usize = transactions.parse().expect("Invalid number of transactions"); @@ -103,9 +108,9 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { .timeout(std::time::Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"); - + let amount: u64 = 1000; for i in 0..num_transactions { - let recipient = format!("{}::1000", address); // 1000 microTari per transaction + let recipient = format!("{}::{}", address, amount); // microTari per transaction let output_path = world.get_temp_path(&format!("tx_{}.json", i)); // Step 1: Create unsigned transaction via CLI @@ -254,8 +259,8 @@ async fn measure_confirmation_time(world: &mut MinotariWorld, _transactions: Str println!("Scan output: {}", world.last_command_output.as_ref().unwrap()); } -#[then("all transactions should be confirmed")] -async fn transactions_confirmed(world: &mut MinotariWorld) { +#[then(regex = r#"^(\d+) transactions of (\d+) uT should be confirmed$"#)] +async fn transactions_confirmed(world: &mut MinotariWorld, count: u64, amount: u64) { assert_eq!( world.last_command_exit_code, Some(0), @@ -263,7 +268,48 @@ async fn transactions_confirmed(world: &mut MinotariWorld) { world.last_command_error.as_deref().unwrap_or("") ); - println!("All transactions confirmed successfully"); + let pre_balance = world.pre_send_balance.expect("Pre-send balance not captured"); + let post_balance = world.fetch_balance(); + let total_sent = count * amount; + + println!("Pre-send balance: {} µT", pre_balance); + println!("Post-send balance: {} µT", post_balance); + println!("Total sent: {} µT ({} x {} µT)", total_sent, count, amount); + + // The post balance includes rewards from newly mined blocks, so it may be + // higher than pre_balance. We verify the sends took effect by checking that + // the balance is at least total_sent less than it would be without sends. + // Since we can't know exact mining rewards, we check that post_balance is + // less than pre_balance + (post_balance - pre_balance + total_sent), i.e., + // the effective decrease from sends is visible. + assert!( + post_balance + total_sent > pre_balance, + "Post balance {} + total sent {} should exceed pre balance {}, \ + indicating funds were received from mining", + post_balance, + total_sent, + pre_balance + ); + + // Verify balance decreased by at least total_sent compared to what it + // would have been without the sends. We estimate the mining reward as + // the difference: mining_reward ≈ post_balance - pre_balance + total_sent + fees. + // If mining_reward > 0 and post_balance < pre_balance + mining_reward, + // then the sends reduced the balance by at least total_sent. + let effective_mining_reward = (post_balance as i128) - (pre_balance as i128) + (total_sent as i128); + assert!( + effective_mining_reward > 0, + "Expected mining rewards to be positive, but balance decreased by more than total sent. \ + pre: {}, post: {}, total_sent: {}", + pre_balance, + post_balance, + total_sent + ); + + println!( + "Confirmed: {} transactions of {} µT sent (estimated mining reward: {} µT)", + count, amount, effective_mining_reward + ); } #[then("I print the benchmark results")] From 2485ab241c9caf645c5c13093a52636a218bc437 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Thu, 12 Mar 2026 15:41:49 +0200 Subject: [PATCH 5/7] flag test --- .github/workflows/ci.yml | 2 +- integration-tests/features/wallet_benchmark.feature | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4265d19..7355c23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: run: cargo test --release --all-targets --all-features --workspace --exclude integration-tests - name: Run integration tests - run: cargo test --release --test cucumber + run: cargo test --release --test cucumber -- -t "not @benchmark" - name: Check unused dependencies run: cargo machete diff --git a/integration-tests/features/wallet_benchmark.feature b/integration-tests/features/wallet_benchmark.feature index 6bfdf37..6841740 100644 --- a/integration-tests/features/wallet_benchmark.feature +++ b/integration-tests/features/wallet_benchmark.feature @@ -3,17 +3,17 @@ Feature: Wallet Performance Benchmarking I want to benchmark wallet performance So that I can measure scanning and transaction confirmation times - @pie + @benchmark Scenario: Benchmark wallet scanning and transaction confirmation performance Given I have a seed node BenchmarkNode And I have a test database with an existing wallet - When I mine 500 blocks on BenchmarkNode - Then I measure the time to scan 500 blocks + When I mine 1000 blocks on BenchmarkNode + Then I measure the time to scan 1000 blocks Then the scan should complete successfully When I check the balance for account "default" Then the balance should be at least 1753895088580 microTari - When I send 10 transactions + When I send 500 transactions And I mine 5 blocks on BenchmarkNode - And I measure the time to confirm 10 transactions - Then 10 transactions of 1000 uT should be confirmed + And I measure the time to confirm 500 transactions + Then 500 transactions of 1000 uT should be confirmed Then I print the benchmark results From d8ebdc48cb9810ec1951e81eef1b6ae0a7e1ee73 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Thu, 12 Mar 2026 15:59:50 +0200 Subject: [PATCH 6/7] fmt --- integration-tests/steps/wallet_benchmark.rs | 57 ++++++++------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs index 9649d72..f61648d 100644 --- a/integration-tests/steps/wallet_benchmark.rs +++ b/integration-tests/steps/wallet_benchmark.rs @@ -10,9 +10,7 @@ use tari_common_types::tari_address::TariAddress; use tari_common_types::tari_address::TariAddressFeatures; use tari_transaction_components::consensus::ConsensusConstantsBuilder; use tari_transaction_components::key_manager::KeyManager; -use tari_transaction_components::offline_signing::models::{ - PrepareOneSidedTransactionForSigningResult, TransactionResult, -}; +use tari_transaction_components::offline_signing::models::TransactionResult; use tari_transaction_components::offline_signing::sign_locked_transaction; use super::common::MinotariWorld; @@ -90,8 +88,7 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { let address = recipient_address.to_base58().to_string(); // Create a key manager from the wallet for offline signing - let key_manager = - KeyManager::new(world.wallet.clone()).expect("Failed to create key manager from wallet"); + let key_manager = KeyManager::new(world.wallet.clone()).expect("Failed to create key manager from wallet"); let consensus_constants = ConsensusConstantsBuilder::new(LocalNet).build(); println!("Sending transactions..."); @@ -103,11 +100,10 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { // Create an HTTP client for submitting transactions to the base node let submit_url = format!("{}/json_rpc", base_url); - let http_client = - reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("Failed to create HTTP client"); + let http_client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client"); let amount: u64 = 1000; for i in 0..num_transactions { let recipient = format!("{}::{}", address, amount); // microTari per transaction @@ -144,28 +140,23 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { }, }; - let unsigned_tx = - match PrepareOneSidedTransactionForSigningResult::from_json(&unsigned_json) { - Ok(tx) => tx, - Err(e) => { - println!("Transaction {} failed to parse unsigned tx: {}", i, e); - continue; - }, - }; - - let signed_result = match sign_locked_transaction( - &key_manager, - consensus_constants.clone(), - LocalNet, - unsigned_tx, - ) { - Ok(result) => result, + let unsigned_tx = match PrepareOneSidedTransactionForSigningResult::from_json(&unsigned_json) { + Ok(tx) => tx, Err(e) => { - println!("Transaction {} signing failed: {}", i, e); + println!("Transaction {} failed to parse unsigned tx: {}", i, e); continue; }, }; + let signed_result = + match sign_locked_transaction(&key_manager, consensus_constants.clone(), LocalNet, unsigned_tx) { + Ok(result) => result, + Err(e) => { + println!("Transaction {} signing failed: {}", i, e); + continue; + }, + }; + // Step 3: Submit the signed transaction to the base node let transaction = signed_result.signed_transaction.transaction; let request = serde_json::json!({ @@ -175,22 +166,14 @@ async fn send_transactions(world: &mut MinotariWorld, transactions: String) { "params": { "transaction": transaction } }); - let submit_result = http_client - .post(&submit_url) - .json(&request) - .send() - .await; + let submit_result = http_client.post(&submit_url).json(&request).send().await; match submit_result { Ok(response) if response.status().is_success() => { successful_txs += 1; }, Ok(response) => { - println!( - "Transaction {} submit failed with status: {}", - i, - response.status() - ); + println!("Transaction {} submit failed with status: {}", i, response.status()); }, Err(e) => { println!("Transaction {} submit failed: {}", i, e); From 974c17dbae8bdabf92aa9798d5357dbae4f47f01 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Thu, 12 Mar 2026 16:40:23 +0200 Subject: [PATCH 7/7] fix --- integration-tests/steps/wallet_benchmark.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/steps/wallet_benchmark.rs b/integration-tests/steps/wallet_benchmark.rs index f61648d..e6a1a53 100644 --- a/integration-tests/steps/wallet_benchmark.rs +++ b/integration-tests/steps/wallet_benchmark.rs @@ -2,6 +2,7 @@ // // Step definitions for benchmarking wallet performance. +use super::common::MinotariWorld; use cucumber::{then, when}; use std::process::Command; use std::time::Instant; @@ -10,11 +11,10 @@ use tari_common_types::tari_address::TariAddress; use tari_common_types::tari_address::TariAddressFeatures; use tari_transaction_components::consensus::ConsensusConstantsBuilder; use tari_transaction_components::key_manager::KeyManager; +use tari_transaction_components::offline_signing::models::PrepareOneSidedTransactionForSigningResult; use tari_transaction_components::offline_signing::models::TransactionResult; use tari_transaction_components::offline_signing::sign_locked_transaction; -use super::common::MinotariWorld; - // ============================= // Benchmark Steps // =============================