From 20b9868acef7671bb0c2da34606804e0eede1b14 Mon Sep 17 00:00:00 2001 From: Moudy Date: Tue, 19 May 2026 00:36:31 +0200 Subject: [PATCH 01/11] feat: add e2e_bench tool for end-to-end scenario latency, block, and tx-byte measurements --- Cargo.lock | 27 ++ Cargo.toml | 2 + docs/benchmarks/README.md | 1 + docs/benchmarks/e2e_bench.md | 125 +++++++++ tools/e2e_bench/Cargo.toml | 33 +++ tools/e2e_bench/README.md | 33 +++ tools/e2e_bench/src/bedrock_handle.rs | 147 +++++++++++ tools/e2e_bench/src/bench_context.rs | 205 +++++++++++++++ tools/e2e_bench/src/harness.rs | 297 ++++++++++++++++++++++ tools/e2e_bench/src/main.rs | 229 +++++++++++++++++ tools/e2e_bench/src/scenarios/amm.rs | 200 +++++++++++++++ tools/e2e_bench/src/scenarios/fanout.rs | 90 +++++++ tools/e2e_bench/src/scenarios/mod.rs | 7 + tools/e2e_bench/src/scenarios/parallel.rs | 188 ++++++++++++++ tools/e2e_bench/src/scenarios/private.rs | 150 +++++++++++ tools/e2e_bench/src/scenarios/token.rs | 127 +++++++++ 16 files changed, 1861 insertions(+) create mode 100644 docs/benchmarks/e2e_bench.md create mode 100644 tools/e2e_bench/Cargo.toml create mode 100644 tools/e2e_bench/README.md create mode 100644 tools/e2e_bench/src/bedrock_handle.rs create mode 100644 tools/e2e_bench/src/bench_context.rs create mode 100644 tools/e2e_bench/src/harness.rs create mode 100644 tools/e2e_bench/src/main.rs create mode 100644 tools/e2e_bench/src/scenarios/amm.rs create mode 100644 tools/e2e_bench/src/scenarios/fanout.rs create mode 100644 tools/e2e_bench/src/scenarios/mod.rs create mode 100644 tools/e2e_bench/src/scenarios/parallel.rs create mode 100644 tools/e2e_bench/src/scenarios/private.rs create mode 100644 tools/e2e_bench/src/scenarios/token.rs diff --git a/Cargo.lock b/Cargo.lock index 57135709..96afcef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2378,6 +2378,33 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "e2e_bench" +version = "0.1.0" +dependencies = [ + "amm_core", + "anyhow", + "borsh", + "chrono", + "clap", + "common", + "indexer_service", + "indexer_service_rpc", + "integration_tests", + "jsonrpsee", + "log", + "nssa", + "nssa_core", + "sequencer_service", + "sequencer_service_rpc", + "serde", + "serde_json", + "tempfile", + "token_core", + "tokio", + "wallet", +] + [[package]] name = "ecdsa" version = "0.16.9" diff --git a/Cargo.toml b/Cargo.toml index 27f3564b..d75e26c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "indexer/ffi", "tools/cycle_bench", "tools/crypto_primitives_bench", + "tools/e2e_bench", ] [workspace.dependencies] @@ -75,6 +76,7 @@ faucet_core = { path = "programs/faucet/core" } vault_core = { path = "programs/vault/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } +integration_tests = { path = "integration_tests" } tokio = { version = "1.50", features = [ "net", diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md index 9289d2bf..db539966 100644 --- a/docs/benchmarks/README.md +++ b/docs/benchmarks/README.md @@ -6,5 +6,6 @@ Bench tools live under `tools/` with READMEs for how to run each one. This direc |---|---| | cycle_bench | [cycle_bench.md](cycle_bench.md) | | crypto_primitives_bench | [crypto_primitives_bench.md](crypto_primitives_bench.md) | +| e2e_bench | [e2e_bench.md](e2e_bench.md) | All numbers are from a single M2 Pro dev box unless noted otherwise. diff --git a/docs/benchmarks/e2e_bench.md b/docs/benchmarks/e2e_bench.md new file mode 100644 index 00000000..2f2a0a7a --- /dev/null +++ b/docs/benchmarks/e2e_bench.md @@ -0,0 +1,125 @@ +# e2e_bench + +End-to-end LEZ scenarios driven through the wallet against an in-process sequencer + indexer wired to an external Bedrock node. Times each step and records borsh sizes per block, split by tx variant. + +## Machine + +| Field | Value | +|---|---| +| Chip | Apple M2 Pro (8P+4E) | +| RAM | 16 GB | +| OS | macOS 15.5 | +| Rust | 1.94.0 | +| Risc0 zkVM | 3.0.5 | +| Profile | release | + +## Scenarios + +| Scenario | Description | +|---|---| +| token | Sequential public token Send + one shielded recipient setup. | +| amm | Pool create, add liquidity, swap, remove liquidity. All public. | +| fanout | One sender → N recipients, sequential. All public. | +| private | Shielded, deshielded, private→private chained private flow. | +| parallel | N senders submit concurrently into one block. All public. | + +## Dev-mode vs real-proving + +`RISC0_DEV_MODE=1` makes the prover emit stub receipts instead of running the recursive STARK pipeline. The table compares each quantity in **dev mode vs real proving** for the two classes of scenarios: + +| Quantity | Public-only scenarios (dev → real) | PPE-bearing scenarios (dev → real) | +|---|---|---| +| Wall time per step | same in both modes | real adds ~100 s per PPE step | +| `public_tx_bytes` | same in both modes | same in both modes | +| `ppe_tx_bytes` | n/a | dev ≈ 2 KB stub → real ≈ 225 KB (matches `S_agg` from cycle_bench) | +| `block_bytes` | same in both modes | real adds ~225 KB per PPE tx in the block | +| `bedrock_finality_ms` | same in both modes | same in both modes (L1 cadence, not LEZ prover) | +| Blocks captured | similar in both modes | real captures more empty clock-only ticks that fill prove wall-time | + +Tables below report dev-mode for all five scenarios. Real-proving numbers are included for `amm_swap_flow` (representative all-public) and `private_chained_flow` (representative chained-private flow); the public-only scenarios converge between modes within run-to-run jitter, so a full real-proving sweep is not run here. + +## Step latencies — dev mode (`RISC0_DEV_MODE=1`) + +Per-scenario wall time and Bedrock L1-finality latency for the closing tip. + +| Scenario | total_ms | total_s | bedrock_finality_ms | bedrock_finality_s | +|---|---:|---:|---:|---:| +| token_onboarding | 60,808 | 60.81 | 24,593 | 24.59 | +| amm_swap_flow | 162,058 | 162.06 | 19,210 | 19.21 | +| multi_recipient_fanout | 222,206 | 222.21 | 16,020 | 16.02 | +| private_chained_flow | 80,700 | 80.70 | 23,963 | 23.96 | +| parallel_fanout | 244,387 | 244.39 | 23,770 | 23.77 | + +Total dev-mode wall time across all five: 912.9 s. + +## Step latencies — real proving (selected scenarios) + +| Scenario | total_ms | total_s | bedrock_finality_ms | bedrock_finality_s | Δ vs dev | +|---|---:|---:|---:|---:|---:| +| amm_swap_flow | 162,437 | 162.44 | ~19,210 | ~19.21 | ~0 (all-public) | +| private_chained_flow | 354,843 | 354.84 | 23,778 | 23.78 | +274.14 s (≈ 91 s per PPE step × 3) | + +Per-step breakdown for `private_chained_flow` in real proving: + +| Step | submit_ms | inclusion_ms | total_ms | total_s | +|---|---:|---:|---:|---:| +| token_new_fungible (public) | 1.1 | 20,276.0 | 20,291.2 | 20.29 | +| shielded_transfer (PPE) | 111,683.3 | 1.0 | 111,730.4 | 111.73 | +| deshielded_transfer (PPE) | 111,454.7 | 1.1 | 111,511.2 | 111.51 | +| private_to_private (PPE) | 111,237.0 | 1.1 | 111,293.0 | 111.29 | + +PPE steps move the cost from `inclusion_ms` (waiting for the next sealed block) to `submit_ms` (the wallet itself proving the PPE circuit before sending). Each PPE prove is ≈ 111 s on this CPU. + +## Block + tx sizes (borsh) — dev mode + +Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. The empty clock-only ticks at `min` give the per-block fixed-cost baseline (≈ 334 bytes across all scenarios). + +| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | +|---|---:|---:|---|---:|---:| +| token_onboarding | 6 | 881 | 334..2,890 | 206 / 8 | 2,556 / 1 | +| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | +| multi_recipient_fanout | 22 | 513 | 334..707 | 221 / 33 | n/a | +| private_chained_flow | 8 | 1,399 | 334..3,565 | 177 / 9 | 2,715 / 3 | +| parallel_fanout | 24 | 646 | 334..3,904 | 248 / 45 | n/a | + +## Block + tx sizes (borsh) — real proving + +| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | +|---|---:|---:|---|---:|---:| +| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | +| private_chained_flow | 35 | 19,692 | 334..226,578 | 159 / 36 | 225,728 / 3 | + +`amm_swap_flow` is byte-identical between dev and real (no proof payload). `private_chained_flow`'s `ppe_tx_bytes` matches the cycle_bench `S_agg` measurement (≈ 225 KB borsh InnerReceipt). The `block_bytes` max (226,578) is the block containing the largest PPE transaction. + +## Findings + +- Public-only scenarios converge between dev mode and real proving in both latency and byte counts. Either mode is suitable to characterize them. +- PPE transactions are ≈ 225 KB on the wire in real proving, dominated by the outer succinct proof. Dev mode emits a ≈ 2 KB stub that does not represent the L1 payload — fee-model storage gas inputs must come from a real-proving run. +- Per-PPE-step prove cost on M2 Pro CPU is ≈ 110-120 s, paid on the wallet side at submit time (not on the sequencer). For a single-program chained flow the cost stacks linearly. +- Empty clock-only ticks set the per-block fixed-cost baseline at ≈ 334 bytes across all scenarios and both modes. +- Bedrock L1 finality stays around 20 s regardless of proving mode, because finality is paced by L1 cadence, not the LEZ prover. + +## Reproduce + +```sh +export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node +export LEZ_BEDROCK_CONFIG_DIR=/path/to/bedrock/configs + +# Dev-mode sweep (fast, ~16 min for all five scenarios) +RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all + +# Real-proving for representative private flow (~6 min on M2 Pro CPU) +cargo run --release -p e2e_bench -- --scenario private + +# Real-proving for representative public flow (~3 min) +cargo run --release -p e2e_bench -- --scenario amm +``` + +JSON output: `target/e2e_bench_dev.json` / `target/e2e_bench_prove.json` (suffix toggled by `RISC0_DEV_MODE`). + +## Caveats + +- Dev-mode `ppe_tx_bytes` and PPE-step latencies are not representative of production; use real-proving numbers for any fee-model input that touches the storage or prover-cost components. +- Single-host run, no GPU acceleration. Real-proving on production prover hardware will move per-step latencies by orders of magnitude; byte counts will not change. +- Bedrock running locally; no real network latency between sequencer and Bedrock. +- Some scenarios share account state via the same wallet; this is intentional (mirrors `integration_tests::TestContext`) and not a realistic multi-wallet workload. diff --git a/tools/e2e_bench/Cargo.toml b/tools/e2e_bench/Cargo.toml new file mode 100644 index 00000000..e3d7fd7a --- /dev/null +++ b/tools/e2e_bench/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "e2e_bench" +version = "0.1.0" +edition = "2024" +license = { workspace = true } +publish = false + +[lints] +workspace = true + +[dependencies] +integration_tests.workspace = true +wallet.workspace = true +nssa.workspace = true +nssa_core = { workspace = true, features = ["host"] } +sequencer_service.workspace = true +sequencer_service_rpc = { workspace = true, features = ["client"] } +indexer_service.workspace = true +indexer_service_rpc = { workspace = true, features = ["client"] } +jsonrpsee = { workspace = true, features = ["ws-client"] } +token_core.workspace = true +amm_core.workspace = true +common.workspace = true +tempfile.workspace = true +borsh.workspace = true +chrono.workspace = true + +anyhow.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } +clap.workspace = true +serde.workspace = true +serde_json.workspace = true +log.workspace = true diff --git a/tools/e2e_bench/README.md b/tools/e2e_bench/README.md new file mode 100644 index 00000000..37d6b175 --- /dev/null +++ b/tools/e2e_bench/README.md @@ -0,0 +1,33 @@ +# e2e_bench + +End-to-end LEZ scenarios driven through the wallet against an in-process sequencer + indexer wired to an external Bedrock node. Times each step (submit, inclusion, wallet sync) and records borsh sizes for every block produced, split into per-tx-variant counts. + +## Run + +Required env vars (no defaults): + +```sh +export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node +export LEZ_BEDROCK_CONFIG_DIR=/path/to/bedrock/configs +# optional: LEZ_BEDROCK_PORT (default 18080) +``` + +The config dir must contain `node-config.yaml` and a `deployment-settings.yaml` template with the literal string `PLACEHOLDER_CHAIN_START_TIME` (rewritten per launch). + +```sh +# All scenarios, dev-mode proving (fast) +RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all + +# One scenario, real proving (slow) +cargo run --release -p e2e_bench -- --scenario amm +``` + +Scenarios: `token`, `amm`, `fanout`, `private`, `parallel`, `all`. + +## What you'll see + +Per scenario: a step table (`submit_ms`, `inclusion_ms`, `sync_ms`, `total_ms`) and a size summary covering every block captured during the scenario (block_bytes total/mean/min/max; per-tx-variant sizes for public, PPE, and program-deployment transactions). + +The fanout, parallel, and private scenarios are the most representative for L1-payload-size measurements since they put multiple txs per block. + +JSON output is written to `target/e2e_bench.json`. diff --git a/tools/e2e_bench/src/bedrock_handle.rs b/tools/e2e_bench/src/bedrock_handle.rs new file mode 100644 index 00000000..ef59d8ad --- /dev/null +++ b/tools/e2e_bench/src/bedrock_handle.rs @@ -0,0 +1,147 @@ +//! Manages an external `logos-blockchain-node` process as a child of the bench. +//! Launches a fresh Bedrock instance per scenario so the indexer never has to +//! catch up a large finalization backlog. +//! +//! Required env vars (no defaults — path layouts differ per developer): +//! - `LEZ_BEDROCK_BIN` — absolute path to the `logos-blockchain-node` binary. +//! - `LEZ_BEDROCK_CONFIG_DIR` — directory containing `node-config.yaml` and +//! `deployment-settings.yaml` (template with `PLACEHOLDER_CHAIN_START_TIME`). +//! +//! Optional: +//! - `LEZ_BEDROCK_PORT` (default: 18080) + +use std::{ + env, + net::SocketAddr, + path::PathBuf, + process::{Child, Command, Stdio}, + time::{Duration, Instant}, +}; + +use anyhow::{Context as _, Result, bail}; + +pub struct BedrockHandle { + child: Option, + addr: SocketAddr, + workdir: PathBuf, +} + +impl BedrockHandle { + /// Launch a fresh Bedrock node. Cleans `state/` in the working dir, rewrites + /// `deployment-settings.yaml` with the current UTC `chain_start_time`, spawns + /// the binary, and polls the HTTP port until ready. + pub async fn launch_fresh() -> Result { + let bin = env::var("LEZ_BEDROCK_BIN").map_err(|err| { + anyhow::anyhow!( + "LEZ_BEDROCK_BIN is required ({err}). Set it to the absolute path of the \ + logos-blockchain-node binary (e.g. \ + `export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node`)." + ) + })?; + let config_dir = env::var("LEZ_BEDROCK_CONFIG_DIR").map_err(|err| { + anyhow::anyhow!( + "LEZ_BEDROCK_CONFIG_DIR is required ({err}). Set it to the directory containing \ + node-config.yaml and deployment-settings.yaml \ + (see tools/e2e_bench/README.md for the expected layout)." + ) + })?; + let port: u16 = env::var("LEZ_BEDROCK_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(18080); + + let bin_path = PathBuf::from(&bin); + if !bin_path.is_file() { + bail!( + "LEZ_BEDROCK_BIN does not point at a file: {bin}. Build it via \ + `cargo build -p logos-blockchain-node --release` in logos-blockchain." + ); + } + let config_dir = PathBuf::from(config_dir); + let node_config = config_dir.join("node-config.yaml"); + let dep_template = config_dir.join("deployment-settings.yaml"); + if !node_config.is_file() || !dep_template.is_file() { + bail!( + "LEZ_BEDROCK_CONFIG_DIR is missing node-config.yaml or \ + deployment-settings.yaml at {}", + config_dir.display() + ); + } + + let workdir = tempfile::tempdir() + .context("create bedrock workdir")? + .keep(); + let dep_runtime = workdir.join("deployment-settings.yaml"); + let raw = std::fs::read_to_string(&dep_template).context("read deployment template")?; + let timestamp = chrono_now_utc_string(); + let filled = raw.replace("PLACEHOLDER_CHAIN_START_TIME", ×tamp); + std::fs::write(&dep_runtime, filled).context("write deployment-settings runtime")?; + + let log_path = workdir.join("bedrock.log"); + let log_file = std::fs::File::create(&log_path).context("create bedrock log")?; + let log_err = log_file.try_clone().context("clone bedrock log")?; + + eprintln!( + "BedrockHandle: launching {} (workdir {})", + bin, + workdir.display() + ); + let child = Command::new(&bin_path) + .current_dir(&workdir) + .arg("--deployment") + .arg(&dep_runtime) + .arg(&node_config) + .env("POL_PROOF_DEV_MODE", "true") + .stdout(Stdio::from(log_file)) + .stderr(Stdio::from(log_err)) + .spawn() + .context("spawn logos-blockchain-node")?; + + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + wait_for_http(addr, Duration::from_secs(60)) + .await + .context("bedrock HTTP did not come up in 60s")?; + + eprintln!("BedrockHandle: stdout/stderr at {}", log_path.display()); + Ok(Self { + child: Some(child), + addr, + workdir, + }) + } + + pub const fn addr(&self) -> SocketAddr { + self.addr + } +} + +impl Drop for BedrockHandle { + fn drop(&mut self) { + if let Some(mut child) = self.child.take() { + eprintln!("BedrockHandle: stopping bedrock pid {}", child.id()); + let _ = child.kill(); + let _ = child.wait(); + } + let _ = std::fs::remove_dir_all(&self.workdir); + } +} + +async fn wait_for_http(addr: SocketAddr, timeout: Duration) -> Result<()> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if tokio::net::TcpStream::connect(addr).await.is_ok() { + // TCP accepts; give Bedrock a moment to finish chain bootstrap. + tokio::time::sleep(Duration::from_secs(2)).await; + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(250)).await; + } + bail!("Bedrock at {addr} did not accept TCP within {timeout:?}"); +} + +fn chrono_now_utc_string() -> String { + // Format: YYYY-MM-DD HH:MM:SS.000000 +00:00:00 (matches the deployment-settings template). + chrono::Utc::now() + .format("%Y-%m-%d %H:%M:%S%.6f +00:00:00") + .to_string() +} diff --git a/tools/e2e_bench/src/bench_context.rs b/tools/e2e_bench/src/bench_context.rs new file mode 100644 index 00000000..43376811 --- /dev/null +++ b/tools/e2e_bench/src/bench_context.rs @@ -0,0 +1,205 @@ +//! BenchContext: wires sequencer + indexer + wallet in-process against an +//! externally-running Bedrock node. Mirrors the surface of +//! `integration_tests::TestContext` for the methods the scenarios need +//! (`wallet_mut()`, `sequencer_client()`), but skips the docker setup. +//! +//! The external Bedrock URL defaults to 127.0.0.1:18080 and can be overridden +//! with the `LEZ_BEDROCK_ADDR` env var. + +use std::{env, net::SocketAddr, path::Path}; + +use anyhow::{Context as _, Result}; +use indexer_service::IndexerHandle; +use integration_tests::config::{ + SequencerPartialConfig, UrlProtocol, addr_to_url, default_private_accounts_for_wallet, + default_public_accounts_for_wallet, genesis_from_accounts, indexer_config, sequencer_config, + wallet_config, +}; +use sequencer_service::SequencerHandle; +use sequencer_service_rpc::{SequencerClient, SequencerClientBuilder}; +use serde::Serialize; +use tempfile::TempDir; +use wallet::{WalletCore, config::WalletConfigOverrides}; + +const DEFAULT_BEDROCK_ADDR: &str = "127.0.0.1:18080"; + +#[expect( + clippy::partial_pub_fields, + reason = "Internal TempDirs are kept alive via private fields for RAII; \ + client and wallet are public for scenarios to drive." +)] +pub struct BenchContext { + pub sequencer_client: SequencerClient, + pub wallet: WalletCore, + #[expect( + dead_code, + reason = "Retained for parity with TestContext; may be needed later." + )] + pub wallet_password: String, + sequencer_handle: Option, + indexer_handle: IndexerHandle, + temp_indexer_dir: TempDir, + temp_sequencer_dir: TempDir, + temp_wallet_dir: TempDir, +} + +impl BenchContext { + pub async fn new() -> Result { + let bedrock_addr_str = + env::var("LEZ_BEDROCK_ADDR").unwrap_or_else(|_| DEFAULT_BEDROCK_ADDR.to_owned()); + let bedrock_addr: SocketAddr = bedrock_addr_str + .parse() + .with_context(|| format!("invalid LEZ_BEDROCK_ADDR `{bedrock_addr_str}`"))?; + + eprintln!("BenchContext: using external bedrock at {bedrock_addr}"); + + let initial_public_accounts = default_public_accounts_for_wallet(); + let initial_private_accounts = default_private_accounts_for_wallet(); + let genesis_transactions = + genesis_from_accounts(&initial_public_accounts, &initial_private_accounts); + let sequencer_partial = SequencerPartialConfig::default(); + + let temp_indexer_dir = tempfile::tempdir().context("indexer temp dir")?; + let indexer_cfg = indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) + .context("indexer config")?; + let indexer_handle = indexer_service::run_server(indexer_cfg, 0) + .await + .context("indexer run_server")?; + + let temp_sequencer_dir = tempfile::tempdir().context("sequencer temp dir")?; + let sequencer_cfg = sequencer_config( + sequencer_partial, + temp_sequencer_dir.path().to_owned(), + bedrock_addr, + genesis_transactions, + ) + .context("sequencer config")?; + let sequencer_handle = sequencer_service::run(sequencer_cfg, 0) + .await + .context("sequencer run")?; + + let temp_wallet_dir = tempfile::tempdir().context("wallet temp dir")?; + let mut wallet_cfg = wallet_config(sequencer_handle.addr()).context("wallet config")?; + // The default 30s poll interval is far too slow for a measurement run; + // shrink so the wallet sees new blocks within ~1s. + wallet_cfg.seq_poll_timeout = std::time::Duration::from_secs(1); + let wallet_cfg_str = + serde_json::to_string_pretty(&wallet_cfg).context("serialize wallet config")?; + let wallet_cfg_path = temp_wallet_dir.path().join("wallet_config.json"); + std::fs::write(&wallet_cfg_path, wallet_cfg_str).context("write wallet config")?; + let storage_path = temp_wallet_dir.path().join("storage.json"); + let password = "bench_pass".to_owned(); + let (mut wallet, _mnemonic) = WalletCore::new_init_storage( + wallet_cfg_path, + storage_path, + Some(WalletConfigOverrides::default()), + &password, + ) + .context("wallet init")?; + // Mirror integration_tests::setup_wallet: import the initial accounts + // produced above so the wallet can reference them by AccountId in scenarios. + for (private_key, _balance) in &initial_public_accounts { + wallet + .storage_mut() + .key_chain_mut() + .add_imported_public_account(private_key.clone()); + } + for private_account in &initial_private_accounts { + wallet + .storage_mut() + .key_chain_mut() + .add_imported_private_account( + private_account.key_chain.clone(), + None, + private_account.identifier, + nssa::Account::default(), + ); + } + wallet + .store_persistent_data() + .context("wallet store persistent")?; + + let sequencer_url = + addr_to_url(UrlProtocol::Http, sequencer_handle.addr()).context("sequencer url")?; + let sequencer_client = SequencerClientBuilder::default() + .build(sequencer_url) + .context("build sequencer client")?; + + Ok(Self { + sequencer_client, + wallet, + wallet_password: password, + sequencer_handle: Some(sequencer_handle), + indexer_handle, + temp_indexer_dir, + temp_sequencer_dir, + temp_wallet_dir, + }) + } + + pub const fn wallet_mut(&mut self) -> &mut WalletCore { + &mut self.wallet + } + + pub const fn sequencer_client(&self) -> &SequencerClient { + &self.sequencer_client + } + + pub fn indexer_addr(&self) -> SocketAddr { + self.indexer_handle.addr() + } + + /// Recursively-sized bytes on disk for sequencer + indexer + wallet tempdirs. + pub fn disk_sizes(&self) -> DiskSizes { + DiskSizes { + sequencer_bytes: dir_size_bytes(self.temp_sequencer_dir.path()), + indexer_bytes: dir_size_bytes(self.temp_indexer_dir.path()), + wallet_bytes: dir_size_bytes(self.temp_wallet_dir.path()), + } + } +} + +#[derive(Debug, Clone, Copy, Default, Serialize)] +#[expect( + clippy::struct_field_names, + reason = "The `_bytes` suffix carries the unit and is preserved verbatim in JSON output." +)] +pub struct DiskSizes { + pub sequencer_bytes: u64, + pub indexer_bytes: u64, + pub wallet_bytes: u64, +} + +fn dir_size_bytes(path: &Path) -> u64 { + let mut total = 0_u64; + let Ok(entries) = std::fs::read_dir(path) else { + return 0; + }; + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_file() { + total = total.saturating_add(metadata.len()); + } else if metadata.is_dir() { + total = total.saturating_add(dir_size_bytes(&entry.path())); + } else { + // Sockets, FIFOs, block/char devices: ignore. Symlinks are + // already followed by `is_file()` / `is_dir()`. + } + } + total +} + +impl Drop for BenchContext { + fn drop(&mut self) { + if let Some(handle) = self.sequencer_handle.take() + && !handle.is_healthy() + { + eprintln!("BenchContext drop: sequencer handle was unhealthy"); + } + if !self.indexer_handle.is_healthy() { + eprintln!("BenchContext drop: indexer handle was unhealthy"); + } + } +} diff --git a/tools/e2e_bench/src/harness.rs b/tools/e2e_bench/src/harness.rs new file mode 100644 index 00000000..bcdff7a7 --- /dev/null +++ b/tools/e2e_bench/src/harness.rs @@ -0,0 +1,297 @@ +//! Step / scenario timing primitives shared across scenarios. + +use std::time::{Duration, Instant}; + +use anyhow::{Result, bail}; +use common::transaction::NSSATransaction; +use sequencer_service_rpc::RpcClient as _; +use serde::Serialize; +use wallet::cli::SubcommandReturnValue; + +use crate::bench_context::BenchContext; + +const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250); +const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120); + +/// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes` +/// is the full Block (header + body + bedrock metadata) and is the closest +/// proxy we have to the L1 payload posted per block. `tx_bytes` is each contained +/// transaction split by variant — this is what the fee model's S_tx slot covers. +#[derive(Debug, Serialize, Clone, Default)] +pub struct BlockSize { + pub block_id: u64, + pub block_bytes: usize, + pub public_tx_bytes: Vec, + pub ppe_tx_bytes: Vec, + pub deploy_tx_bytes: Vec, +} + +#[derive(Debug, Serialize, Clone)] +pub struct StepResult { + pub label: String, + pub submit_ms: f64, + pub inclusion_ms: Option, + pub wallet_sync_ms: Option, + pub total_ms: f64, + pub tx_hash: Option, + /// Borsh sizes for every zone block produced during this step. + /// Empty for steps that don't advance the chain (e.g. RegisterAccount). + pub blocks: Vec, +} + +#[derive(Debug, Serialize, Default)] +pub struct ScenarioResult { + pub name: String, + pub setup_ms: f64, + pub steps: Vec, + pub total_ms: f64, + /// Disk sizes (sequencer / indexer / wallet tempdirs) sampled at scenario start. + pub disk_before: Option, + /// Disk sizes sampled at scenario end. + pub disk_after: Option, + /// Bedrock-finality latency: time from final-step inclusion to the indexer + /// reporting the sequencer tip as L1-finalised. Effectively measures the + /// sequencer→Bedrock posting + Bedrock finalisation + indexer L1 ingest path. + /// A value at the timeout (60s) means finalisation did not happen within the bench window. + pub bedrock_finality_ms: Option, +} + +impl ScenarioResult { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + ..Default::default() + } + } + + pub fn push(&mut self, step: StepResult) { + self.total_ms += step.total_ms; + self.steps.push(step); + } +} + +/// Finish a timed wallet step. Records submit (the time between `started` +/// being captured and `ret` being received) and, if `ret` is a +/// [`SubcommandReturnValue::PrivacyPreservingTransfer`], polls the sequencer +/// for inclusion and records the inclusion latency. Returns a [`StepResult`]. +/// +/// Usage: +/// ```ignore +/// let started = Instant::now(); +/// let ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), cmd).await?; +/// let step = finalize_step("label", started, ret, ctx).await?; +/// ``` +/// Begin a timed step. Capture this *before* submitting the wallet operation +/// so we can later subtract it from the post-submit block height to detect +/// when the chain has advanced past the tx's block. +pub async fn begin_step(ctx: &BenchContext) -> Result { + Ok(ctx.sequencer_client().get_last_block_id().await?) +} + +pub async fn finalize_step( + label: impl Into, + started: Instant, + pre_block_id: u64, + ret: &SubcommandReturnValue, + ctx: &mut BenchContext, +) -> Result { + let label = label.into(); + let submit_ms = started.elapsed().as_secs_f64() * 1_000.0; + + let mut tx_hash_str = None; + let mut inclusion_ms = None; + let mut wallet_sync_ms = None; + let mut blocks: Vec = Vec::new(); + + // For non-account-create steps (anything that produces a tx_hash, or even + // `Empty` for public Token Send), wait for the chain to advance past the + // submission block so state is applied before the next step. We use + // get_last_block_id as the canonical "block has been produced and + // recorded" signal. + let should_wait_for_chain = !matches!(ret, SubcommandReturnValue::RegisterAccount { .. }); + if should_wait_for_chain { + if let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = ret { + tx_hash_str = Some(format!("{tx_hash}")); + } + let started_inclusion = Instant::now(); + wait_for_chain_advance(ctx, pre_block_id, 2).await?; + inclusion_ms = Some(started_inclusion.elapsed().as_secs_f64() * 1_000.0); + + let started_sync = Instant::now(); + sync_wallet_to_tip(ctx).await?; + wallet_sync_ms = Some(started_sync.elapsed().as_secs_f64() * 1_000.0); + + // Capture block-byte and per-tx-byte sizes for every block produced + // during this step. We intentionally capture all blocks, including + // empty clock-only ticks: the empty-block baseline lets the fee model + // back out the per-tx contribution. + let tip = ctx.sequencer_client().get_last_block_id().await?; + for block_id in (pre_block_id.saturating_add(1))..=tip { + if let Some(block) = ctx.sequencer_client().get_block(block_id).await? { + let block_bytes = borsh::to_vec(&block).map_or(0, |v| v.len()); + let mut sz = BlockSize { + block_id, + block_bytes, + public_tx_bytes: Vec::new(), + ppe_tx_bytes: Vec::new(), + deploy_tx_bytes: Vec::new(), + }; + for tx in &block.body.transactions { + let n = borsh::to_vec(tx).map_or(0, |v| v.len()); + match tx { + NSSATransaction::Public(_) => sz.public_tx_bytes.push(n), + NSSATransaction::PrivacyPreserving(_) => sz.ppe_tx_bytes.push(n), + NSSATransaction::ProgramDeployment(_) => sz.deploy_tx_bytes.push(n), + } + } + blocks.push(sz); + } + } + } + + Ok(StepResult { + label, + submit_ms, + inclusion_ms, + wallet_sync_ms, + total_ms: started.elapsed().as_secs_f64() * 1_000.0, + tx_hash: tx_hash_str, + blocks, + }) +} + +/// Wait for `get_last_block_id` to advance by at least `min_blocks` from `from_block_id`. +pub async fn wait_for_chain_advance( + ctx: &BenchContext, + from_block_id: u64, + min_blocks: u64, +) -> Result<()> { + let target = from_block_id.saturating_add(min_blocks); + let deadline = Instant::now() + TX_INCLUSION_TIMEOUT; + loop { + match ctx.sequencer_client().get_last_block_id().await { + Ok(current) if current >= target => return Ok(()), + Ok(_) => {} + Err(err) => eprintln!("get_last_block_id error (continuing poll): {err:#}"), + } + if Instant::now() > deadline { + bail!( + "chain did not advance from {from_block_id} to at least {target} within {TX_INCLUSION_TIMEOUT:?}" + ); + } + tokio::time::sleep(TX_INCLUSION_POLL_INTERVAL).await; + } +} + +async fn sync_wallet_to_tip(ctx: &mut BenchContext) -> Result<()> { + let last_block = ctx.sequencer_client().get_last_block_id().await?; + ctx.wallet_mut().sync_to_block(last_block).await?; + Ok(()) +} + +pub fn print_table(result: &ScenarioResult) { + let label_width = result + .steps + .iter() + .map(|s| s.label.len()) + .max() + .unwrap_or(0) + .max("step".len()); + + println!( + "\nScenario: {} (setup {:.1} ms ({:.2}s), total {:.1} ms ({:.2}s))", + result.name, + result.setup_ms, + result.setup_ms / 1_000.0, + result.total_ms, + result.total_ms / 1_000.0, + ); + println!( + "{:10} {:>12} {:>10} {:>16}", + "step", + "submit_ms", + "inclusion_ms", + "sync_ms", + "total_ms (s)", + lw = label_width, + ); + println!("{}", "-".repeat(label_width + 62)); + for s in &result.steps { + let inclusion = s + .inclusion_ms + .map_or_else(|| "-".to_owned(), |v| format!("{v:.1}")); + let sync = s + .wallet_sync_ms + .map_or_else(|| "-".to_owned(), |v| format!("{v:.1}")); + let total = format!("{:.1} ({:.2}s)", s.total_ms, s.total_ms / 1_000.0); + println!( + "{:10.1} {:>12} {:>10} {:>16}", + s.label, + s.submit_ms, + inclusion, + sync, + total, + lw = label_width, + ); + } + + print_size_summary(result); +} + +/// Aggregate borsh sizes per scenario: total/mean/min/max block bytes, and +/// per-tx bytes split by variant. Empty if no blocks were captured. +fn print_size_summary(result: &ScenarioResult) { + let blocks: Vec<&BlockSize> = result.steps.iter().flat_map(|s| s.blocks.iter()).collect(); + if blocks.is_empty() { + return; + } + + let block_bytes: Vec = blocks.iter().map(|b| b.block_bytes).collect(); + let total_block_bytes: usize = block_bytes.iter().sum(); + let mean_block = mean_usize(&block_bytes); + let min_block = block_bytes.iter().copied().min().unwrap_or(0); + let max_block = block_bytes.iter().copied().max().unwrap_or(0); + + let public: Vec = blocks + .iter() + .flat_map(|b| b.public_tx_bytes.iter().copied()) + .collect(); + let ppe: Vec = blocks + .iter() + .flat_map(|b| b.ppe_tx_bytes.iter().copied()) + .collect(); + let deploy: Vec = blocks + .iter() + .flat_map(|b| b.deploy_tx_bytes.iter().copied()) + .collect(); + + println!( + "\nBlock + tx size summary ({} blocks captured):", + blocks.len() + ); + println!( + " block_bytes: total={total_block_bytes}, mean={mean_block}, min={min_block}, max={max_block}", + ); + print_tx_line("public_tx_bytes ", &public); + print_tx_line("ppe_tx_bytes ", &ppe); + print_tx_line("deploy_tx_bytes ", &deploy); +} + +fn print_tx_line(label: &str, samples: &[usize]) { + if samples.is_empty() { + println!(" {label}: (none)"); + return; + } + let total: usize = samples.iter().sum(); + let mean = mean_usize(samples); + let min = samples.iter().copied().min().unwrap_or(0); + let max = samples.iter().copied().max().unwrap_or(0); + println!( + " {label}: n={}, total={total}, mean={mean}, min={min}, max={max}", + samples.len() + ); +} + +fn mean_usize(xs: &[usize]) -> usize { + xs.iter().sum::().checked_div(xs.len()).unwrap_or(0) +} diff --git a/tools/e2e_bench/src/main.rs b/tools/e2e_bench/src/main.rs new file mode 100644 index 00000000..31ea1189 --- /dev/null +++ b/tools/e2e_bench/src/main.rs @@ -0,0 +1,229 @@ +//! End-to-end LEZ scenario bench. +//! +//! Spins up the full stack (native Bedrock node launched per-scenario via +//! `BedrockHandle` + in-process sequencer + indexer + wallet via +//! `BenchContext`) and drives the wallet through configurable scenarios that +//! mirror real user flows. Times each step and records borsh-serialized +//! block + tx sizes per scenario. +//! +//! Required env vars (no defaults; see `tools/e2e_bench/README.md`): +//! LEZ_BEDROCK_BIN absolute path to logos-blockchain-node. +//! LEZ_BEDROCK_CONFIG_DIR directory with node-config.yaml + deployment template. +//! +//! Run examples: +//! RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all. +//! cargo run --release -p e2e_bench -- --scenario amm. +//! +//! `RISC0_DEV_MODE=1` skips proving and produces latency-only numbers in +//! ~minutes; omitting it produces realistic proving-inclusive numbers but +//! the run takes much longer. + +#![expect( + clippy::arbitrary_source_item_ordering, + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::doc_markdown, + clippy::float_arithmetic, + clippy::let_underscore_must_use, + clippy::let_underscore_untyped, + clippy::missing_const_for_fn, + clippy::print_stderr, + clippy::print_stdout, + clippy::single_call_fn, + clippy::single_match_else, + clippy::std_instead_of_core, + clippy::too_many_lines, + clippy::wildcard_enum_match_arm, + reason = "Bench tool: matches test-style fixture code" +)] + +use std::{path::PathBuf, time::Duration}; + +use anyhow::{Context as _, Result}; +use bedrock_handle::BedrockHandle; +use bench_context::BenchContext; +use clap::{Parser, ValueEnum}; +use harness::ScenarioResult; +use serde::Serialize; + +mod bedrock_handle; +mod bench_context; +mod harness; +mod scenarios; + +#[derive(Copy, Clone, Debug, ValueEnum)] +enum ScenarioName { + Token, + Amm, + Fanout, + Private, + Parallel, + All, +} + +#[derive(Parser, Debug)] +#[command(about = "End-to-end LEZ scenario bench")] +struct Cli { + /// Which scenario(s) to run. + #[arg(long, value_enum, default_value_t = ScenarioName::All)] + scenario: ScenarioName, + + /// Optional JSON output path. Defaults to /target/e2e_bench.json. + #[arg(long)] + json_out: Option, +} + +#[derive(Debug, Serialize)] +struct BenchRunReport { + risc0_dev_mode: bool, + scenarios: Vec, + total_wall_seconds: f64, +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<()> { + // integration_tests initializes env_logger via a LazyLock, so we leave logger + // setup to it. Set RUST_LOG=info before running to see logs. + + let cli = Cli::parse(); + let risc0_dev_mode = std::env::var("RISC0_DEV_MODE").is_ok_and(|v| !v.is_empty() && v != "0"); + + eprintln!( + "e2e_bench: scenario={:?}, RISC0_DEV_MODE={}", + cli.scenario, + if risc0_dev_mode { "1" } else { "unset/0" } + ); + + let to_run: Vec = match cli.scenario { + ScenarioName::All => vec![ + ScenarioName::Token, + ScenarioName::Amm, + ScenarioName::Fanout, + ScenarioName::Private, + ScenarioName::Parallel, + ], + other => vec![other], + }; + + let overall_started = std::time::Instant::now(); + let mut all_results = Vec::with_capacity(to_run.len()); + + for name in to_run { + eprintln!("\n=== running scenario: {name:?} ==="); + let setup_started = std::time::Instant::now(); + // Spawn a fresh Bedrock node for this scenario. Each scenario therefore + // starts with an empty chain so the indexer never has a backlog from a + // prior scenario. + let bedrock = BedrockHandle::launch_fresh() + .await + .with_context(|| format!("failed to spawn Bedrock for scenario {name:?}"))?; + let bedrock_addr_string = format!("{}", bedrock.addr()); + // Safety: we restore the previous LEZ_BEDROCK_ADDR value (if any) at scenario teardown. + // SAFETY: this happens before any threaded setup that reads env. + unsafe { + std::env::set_var("LEZ_BEDROCK_ADDR", &bedrock_addr_string); + } + + let mut ctx = BenchContext::new() + .await + .with_context(|| format!("failed to setup BenchContext for scenario {name:?}"))?; + let setup_ms = elapsed_ms(setup_started); + eprintln!("setup: {setup_ms:.1} ms"); + + let disk_before = ctx.disk_sizes(); + let mut result = run_scenario(name, setup_ms, &mut ctx).await?; + result.disk_before = Some(disk_before); + result.disk_after = Some(ctx.disk_sizes()); + result.bedrock_finality_ms = Some(measure_bedrock_finality(&ctx).await?); + harness::print_table(&result); + all_results.push(result); + + drop(ctx); + drop(bedrock); + // Give Bedrock a moment to shut down before the next scenario. + tokio::time::sleep(Duration::from_secs(2)).await; + } + + let total_wall_seconds = overall_started.elapsed().as_secs_f64(); + eprintln!("\nTotal wall time: {total_wall_seconds:.1}s"); + + let report = BenchRunReport { + risc0_dev_mode, + scenarios: all_results, + total_wall_seconds, + }; + + let out_path = match cli.json_out { + Some(p) => p, + None => { + let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .canonicalize()?; + let suffix = if risc0_dev_mode { "dev" } else { "prove" }; + workspace_root + .join("target") + .join(format!("e2e_bench_{suffix}.json")) + } + }; + if let Some(parent) = out_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&out_path, serde_json::to_string_pretty(&report)?)?; + eprintln!("\nJSON written to {}", out_path.display()); + + Ok(()) +} + +async fn run_scenario( + name: ScenarioName, + setup_ms: f64, + ctx: &mut BenchContext, +) -> Result { + let result = match name { + ScenarioName::Token => scenarios::token::run(ctx).await?, + ScenarioName::Amm => scenarios::amm::run(ctx).await?, + ScenarioName::Fanout => scenarios::fanout::run(ctx).await?, + ScenarioName::Private => scenarios::private::run(ctx).await?, + ScenarioName::Parallel => scenarios::parallel::run(ctx).await?, + ScenarioName::All => unreachable!("dispatched above"), + }; + Ok(ScenarioResult { setup_ms, ..result }) +} + +fn elapsed_ms(t: std::time::Instant) -> f64 { + t.elapsed().as_secs_f64() * 1_000.0 +} + +/// Poll the indexer's L1-finalised block id until it catches up with the +/// sequencer's last block id. This is effectively the sequencer→Bedrock posting +/// plus Bedrock finalisation plus indexer ingest latency. +async fn measure_bedrock_finality(ctx: &BenchContext) -> Result { + use indexer_service_rpc::RpcClient as _; + use jsonrpsee::ws_client::WsClientBuilder; + use sequencer_service_rpc::RpcClient as _; + + let indexer_url = format!("ws://{}", ctx.indexer_addr()); + let indexer_ws = WsClientBuilder::default() + .build(&indexer_url) + .await + .context("connect indexer WS")?; + let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?; + + let started = std::time::Instant::now(); + let deadline = started + Duration::from_secs(60); + loop { + match indexer_ws.get_last_finalized_block_id().await { + Ok(Some(b)) if b >= sequencer_tip => { + return Ok(started.elapsed().as_secs_f64() * 1_000.0); + } + Ok(_) => {} + Err(err) => eprintln!("indexer last_synced poll error: {err:#}"), + } + if std::time::Instant::now() > deadline { + eprintln!("indexer did not catch up to {sequencer_tip} within 60s"); + return Ok(started.elapsed().as_secs_f64() * 1_000.0); + } + tokio::time::sleep(Duration::from_millis(200)).await; + } +} diff --git a/tools/e2e_bench/src/scenarios/amm.rs b/tools/e2e_bench/src/scenarios/amm.rs new file mode 100644 index 00000000..b92bd823 --- /dev/null +++ b/tools/e2e_bench/src/scenarios/amm.rs @@ -0,0 +1,200 @@ +//! AMM swap flow: setup two tokens, create pool, swap, add liquidity, remove liquidity. + +use std::time::Instant; + +use anyhow::{Result, bail}; +use integration_tests::public_mention; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, +}; + +use crate::harness::{ScenarioResult, finalize_step}; + +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut result = ScenarioResult::new("amm_swap_flow"); + + let def_a = new_public_account(ctx, &mut result, "create_acc_def_a").await?; + let supply_a = new_public_account(ctx, &mut result, "create_acc_supply_a").await?; + let user_a = new_public_account(ctx, &mut result, "create_acc_user_a").await?; + + let def_b = new_public_account(ctx, &mut result, "create_acc_def_b").await?; + let supply_b = new_public_account(ctx, &mut result, "create_acc_supply_b").await?; + let user_b = new_public_account(ctx, &mut result, "create_acc_user_b").await?; + + let user_lp = new_public_account(ctx, &mut result, "create_acc_user_lp").await?; + + timed_token_new(ctx, &mut result, "token_a_new", def_a, supply_a, "TokA").await?; + timed_token_send( + ctx, + &mut result, + "token_a_fund_user", + supply_a, + user_a, + 1_000, + ) + .await?; + + timed_token_new(ctx, &mut result, "token_b_new", def_b, supply_b, "TokB").await?; + timed_token_send( + ctx, + &mut result, + "token_b_fund_user", + supply_b, + user_b, + 1_000, + ) + .await?; + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::New { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + balance_a: 300, + balance_b: 300, + }), + ) + .await?; + let step = finalize_step("amm_new_pool", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::SwapExactInput { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + amount_in: 50, + min_amount_out: 1, + token_definition: def_a, + }), + ) + .await?; + let step = finalize_step("amm_swap_exact_input", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::AddLiquidity { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + min_amount_lp: 1, + max_amount_a: 100, + max_amount_b: 100, + }), + ) + .await?; + let step = finalize_step("amm_add_liquidity", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::RemoveLiquidity { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + balance_lp: 50, + min_amount_a: 1, + min_amount_b: 1, + }), + ) + .await?; + let step = finalize_step("amm_remove_liquidity", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + Ok(result) +} + +async fn new_public_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} + +async fn timed_token_new( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, + def_id: nssa::AccountId, + supply_id: nssa::AccountId, + name: &str, +) -> Result<()> { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name: name.to_owned(), + total_supply: 10_000, + }), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + Ok(()) +} + +async fn timed_token_send( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, + from_id: nssa::AccountId, + to_id: nssa::AccountId, + amount: u128, +) -> Result<()> { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(from_id), + to: Some(public_mention(to_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount, + }), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + Ok(()) +} diff --git a/tools/e2e_bench/src/scenarios/fanout.rs b/tools/e2e_bench/src/scenarios/fanout.rs new file mode 100644 index 00000000..adede185 --- /dev/null +++ b/tools/e2e_bench/src/scenarios/fanout.rs @@ -0,0 +1,90 @@ +//! Multi-recipient fanout: one funded supply pays 10 distinct recipients. + +use std::time::Instant; + +use anyhow::{Result, bail}; +use integration_tests::public_mention; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::token::TokenProgramAgnosticSubcommand, +}; + +use crate::harness::{ScenarioResult, finalize_step}; + +const FANOUT_COUNT: usize = 10; +const AMOUNT_PER_TRANSFER: u128 = 100; + +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut result = ScenarioResult::new("multi_recipient_fanout"); + + let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; + let supply_id = new_public_account(ctx, &mut result, "create_acc_supply").await?; + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name: "FanoutToken".to_owned(), + total_supply: 10_000_000, + }), + ) + .await?; + let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + let mut recipients = Vec::with_capacity(FANOUT_COUNT); + for i in 0..FANOUT_COUNT { + let id = new_public_account(ctx, &mut result, &format!("create_recipient_{i:02}")).await?; + recipients.push(id); + } + + for (i, recipient_id) in recipients.iter().enumerate() { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(public_mention(*recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: AMOUNT_PER_TRANSFER, + }), + ) + .await?; + let step = finalize_step(format!("transfer_{i:02}"), started, pre_block, &ret, ctx).await?; + result.push(step); + } + + Ok(result) +} + +async fn new_public_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} diff --git a/tools/e2e_bench/src/scenarios/mod.rs b/tools/e2e_bench/src/scenarios/mod.rs new file mode 100644 index 00000000..0536a250 --- /dev/null +++ b/tools/e2e_bench/src/scenarios/mod.rs @@ -0,0 +1,7 @@ +//! Scenarios driven by the e2e bench. + +pub mod amm; +pub mod fanout; +pub mod parallel; +pub mod private; +pub mod token; diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/e2e_bench/src/scenarios/parallel.rs new file mode 100644 index 00000000..43bd25ac --- /dev/null +++ b/tools/e2e_bench/src/scenarios/parallel.rs @@ -0,0 +1,188 @@ +//! Parallel-fanout throughput scenario. N distinct senders each transfer one token +//! to one recipient. Submission is serialised through the single wallet but does +//! not wait for chain advance between submits, so all N txs land in the same +//! block (up to `max_num_tx_in_block`). Measures observed throughput. + +use std::time::Instant; + +use anyhow::{Result, bail}; +use common::transaction::NSSATransaction; +use integration_tests::public_mention; +use sequencer_service_rpc::RpcClient as _; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::token::TokenProgramAgnosticSubcommand, +}; + +use crate::{ + bench_context::BenchContext, + harness::{BlockSize, ScenarioResult, StepResult, finalize_step}, +}; + +const PARALLEL_FANOUT_N: usize = 10; +const AMOUNT_PER_TRANSFER: u128 = 100; + +pub async fn run(ctx: &mut BenchContext) -> Result { + let mut result = ScenarioResult::new("parallel_fanout"); + + // Setup: definition, master supply, N parallel supplies, N recipients. + let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; + let master_id = new_public_account(ctx, &mut result, "create_acc_master").await?; + + let mut senders = Vec::with_capacity(PARALLEL_FANOUT_N); + for i in 0..PARALLEL_FANOUT_N { + let id = new_public_account(ctx, &mut result, &format!("create_sender_{i:02}")).await?; + senders.push(id); + } + let mut recipients = Vec::with_capacity(PARALLEL_FANOUT_N); + for i in 0..PARALLEL_FANOUT_N { + let id = new_public_account(ctx, &mut result, &format!("create_recipient_{i:02}")).await?; + recipients.push(id); + } + + // Mint full supply into master. + let total_mint: u128 = (PARALLEL_FANOUT_N as u128) * AMOUNT_PER_TRANSFER * 10; + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(master_id), + name: "ParToken".to_owned(), + total_supply: total_mint, + }), + ) + .await?; + let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + // Fund each sender from master. Serial; this is setup, not measured throughput. + for (i, sender_id) in senders.iter().enumerate() { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(master_id), + to: Some(public_mention(*sender_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: AMOUNT_PER_TRANSFER * 5, + }), + ) + .await?; + let step = + finalize_step(format!("fund_sender_{i:02}"), started, pre_block, &ret, ctx).await?; + result.push(step); + } + + // The measured phase: submit N transfers as fast as possible, do not wait + // for chain advance between submits. The sequencer batches whatever lands in + // its mempool before block_create_timeout. + let pre_block_burst = ctx.sequencer_client().get_last_block_id().await?; + let burst_started = Instant::now(); + + // Submit all N back-to-back. Wallet serialises through `wallet_mut()`, but + // each sender has its own nonce so there are no collisions. + for (sender_id, recipient_id) in senders.iter().zip(recipients.iter()) { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(*sender_id), + to: Some(public_mention(*recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: AMOUNT_PER_TRANSFER, + }), + ) + .await?; + } + let all_submitted_at = Instant::now(); + let submit_duration_ms = (all_submitted_at - burst_started).as_secs_f64() * 1_000.0; + + // Wait for the chain to advance by at least 2 blocks past pre_block_burst. + // That guarantees the block holding our burst is sealed and applied. + crate::harness::wait_for_chain_advance(ctx, pre_block_burst, 2).await?; + let inclusion_done_at = Instant::now(); + let inclusion_after_submit_ms = (inclusion_done_at - all_submitted_at).as_secs_f64() * 1_000.0; + let burst_total_ms = (inclusion_done_at - burst_started).as_secs_f64() * 1_000.0; + + eprintln!( + "parallel_fanout: submitted {} txs in {:.1} ms, inclusion in {:.1} ms, total {:.1} ms", + senders.len(), + submit_duration_ms, + inclusion_after_submit_ms, + burst_total_ms, + ); + + // Capture every block produced during the burst window. This is the + // scenario where one block holds many txs, so block_bytes here is the + // most representative L1-payload-equivalent measurement we have. + let tip = ctx.sequencer_client().get_last_block_id().await?; + let mut blocks: Vec = Vec::new(); + for block_id in (pre_block_burst.saturating_add(1))..=tip { + if let Some(block) = ctx.sequencer_client().get_block(block_id).await? { + let block_bytes = borsh::to_vec(&block).map_or(0, |v| v.len()); + let mut sz = BlockSize { + block_id, + block_bytes, + public_tx_bytes: Vec::new(), + ppe_tx_bytes: Vec::new(), + deploy_tx_bytes: Vec::new(), + }; + for tx in &block.body.transactions { + let n = borsh::to_vec(tx).map_or(0, |v| v.len()); + match tx { + NSSATransaction::Public(_) => sz.public_tx_bytes.push(n), + NSSATransaction::PrivacyPreserving(_) => sz.ppe_tx_bytes.push(n), + NSSATransaction::ProgramDeployment(_) => sz.deploy_tx_bytes.push(n), + } + } + blocks.push(sz); + } + } + + // Synthesise a single summary "step" for the burst. Use the submit time + // for `submit_ms` and the inclusion-wait time for `inclusion_ms`. + let burst_step = StepResult { + label: format!("burst_{}_transfers", senders.len()), + submit_ms: submit_duration_ms, + inclusion_ms: Some(inclusion_after_submit_ms), + wallet_sync_ms: None, + total_ms: burst_total_ms, + tx_hash: None, + blocks, + }; + result.push(burst_step); + + Ok(result) +} + +async fn new_public_account( + ctx: &mut BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} diff --git a/tools/e2e_bench/src/scenarios/private.rs b/tools/e2e_bench/src/scenarios/private.rs new file mode 100644 index 00000000..2a154673 --- /dev/null +++ b/tools/e2e_bench/src/scenarios/private.rs @@ -0,0 +1,150 @@ +//! Private chained flow: shielded, deshielded, and private-to-private transfers. + +use std::time::Instant; + +use anyhow::{Result, bail}; +use integration_tests::{private_mention, public_mention}; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::token::TokenProgramAgnosticSubcommand, +}; + +use crate::harness::{ScenarioResult, finalize_step}; + +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut result = ScenarioResult::new("private_chained_flow"); + + let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; + let supply_id = new_public_account(ctx, &mut result, "create_acc_supply").await?; + let public_recipient_id = + new_public_account(ctx, &mut result, "create_acc_pub_recipient").await?; + let private_a = new_private_account(ctx, &mut result, "create_acc_priv_a").await?; + let private_b = new_private_account(ctx, &mut result, "create_acc_priv_b").await?; + + // Mint into public supply. + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name: "PrivToken".to_owned(), + total_supply: 1_000_000, + }), + ) + .await?; + let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + // Shielded transfer: public supply -> private_a. + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(private_mention(private_a)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 1_000, + }), + ) + .await?; + let step = finalize_step("shielded_transfer", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + // Deshielded transfer: private_a -> public_recipient. + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: private_mention(private_a), + to: Some(public_mention(public_recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 100, + }), + ) + .await?; + let step = finalize_step("deshielded_transfer", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + // Private-to-private transfer: private_a -> private_b. + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: private_mention(private_a), + to: Some(private_mention(private_b)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 200, + }), + ) + .await?; + let step = finalize_step("private_to_private", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + Ok(result) +} + +async fn new_public_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} + +async fn new_private_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} diff --git a/tools/e2e_bench/src/scenarios/token.rs b/tools/e2e_bench/src/scenarios/token.rs new file mode 100644 index 00000000..c81bd8c1 --- /dev/null +++ b/tools/e2e_bench/src/scenarios/token.rs @@ -0,0 +1,127 @@ +//! Token onboarding scenario: create accounts, mint, public transfer, private transfer. + +use std::time::Instant; + +use anyhow::{Result, bail}; +use integration_tests::{private_mention, public_mention}; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::token::TokenProgramAgnosticSubcommand, +}; + +use crate::harness::{ScenarioResult, finalize_step}; + +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut result = ScenarioResult::new("token_onboarding"); + + let definition_id = new_public_account(ctx, &mut result, "create_pub_definition").await?; + let supply_id = new_public_account(ctx, &mut result, "create_pub_supply").await?; + let recipient_id = new_public_account(ctx, &mut result, "create_pub_recipient").await?; + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(definition_id), + supply_account_id: public_mention(supply_id), + name: "BenchToken".to_owned(), + total_supply: 1_000_000, + }), + ) + .await?; + let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(public_mention(recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 1_000, + }), + ) + .await?; + let step = finalize_step("token_public_transfer", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + let private_recipient_id = + new_private_account(ctx, &mut result, "create_priv_recipient").await?; + + { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(private_mention(private_recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 500, + }), + ) + .await?; + let step = finalize_step("token_shielded_transfer", started, pre_block, &ret, ctx).await?; + result.push(step); + } + + Ok(result) +} + +async fn new_public_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} + +async fn new_private_account( + ctx: &mut crate::bench_context::BenchContext, + result: &mut ScenarioResult, + label: &str, +) -> Result { + let pre_block = crate::harness::begin_step(ctx).await?; + let started = Instant::now(); + let ret = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + result.push(step); + match ret { + SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), + other => bail!("expected RegisterAccount, got {other:?}"), + } +} From 832b21f74d8c9866dac4310a7f3f8a8be565ddba Mon Sep 17 00:00:00 2001 From: Moudy Date: Tue, 19 May 2026 09:59:11 +0200 Subject: [PATCH 02/11] fix: cli --- .deny.toml | 1 + tools/e2e_bench/Cargo.toml | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.deny.toml b/.deny.toml index fb1ce3cf..320a9eda 100644 --- a/.deny.toml +++ b/.deny.toml @@ -16,6 +16,7 @@ ignore = [ { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, + { id = "RUSTSEC-2026-0145", reason = "`astral-tokio-tar` v0.6.1 is pulled transitively via testcontainers (integration_tests dev/test path); waiting on upstream fix" }, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/tools/e2e_bench/Cargo.toml b/tools/e2e_bench/Cargo.toml index e3d7fd7a..c6658a8e 100644 --- a/tools/e2e_bench/Cargo.toml +++ b/tools/e2e_bench/Cargo.toml @@ -12,14 +12,11 @@ workspace = true integration_tests.workspace = true wallet.workspace = true nssa.workspace = true -nssa_core = { workspace = true, features = ["host"] } sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } indexer_service.workspace = true indexer_service_rpc = { workspace = true, features = ["client"] } jsonrpsee = { workspace = true, features = ["ws-client"] } -token_core.workspace = true -amm_core.workspace = true common.workspace = true tempfile.workspace = true borsh.workspace = true @@ -30,4 +27,3 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } clap.workspace = true serde.workspace = true serde_json.workspace = true -log.workspace = true From c3daa9897d75e20615c5e2eca437e705ccba794c Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 19 May 2026 18:54:11 +0200 Subject: [PATCH 03/11] docs(e2e_bench): drop machine table and stale benchmark numbers --- docs/benchmarks/e2e_bench.md | 79 ++++-------------------------------- 1 file changed, 7 insertions(+), 72 deletions(-) diff --git a/docs/benchmarks/e2e_bench.md b/docs/benchmarks/e2e_bench.md index 2f2a0a7a..278dab9b 100644 --- a/docs/benchmarks/e2e_bench.md +++ b/docs/benchmarks/e2e_bench.md @@ -2,16 +2,7 @@ End-to-end LEZ scenarios driven through the wallet against an in-process sequencer + indexer wired to an external Bedrock node. Times each step and records borsh sizes per block, split by tx variant. -## Machine - -| Field | Value | -|---|---| -| Chip | Apple M2 Pro (8P+4E) | -| RAM | 16 GB | -| OS | macOS 15.5 | -| Rust | 1.94.0 | -| Risc0 zkVM | 3.0.5 | -| Profile | release | +No numeric tables here yet. Absolute wall time and block sizes depend heavily on the bedrock config (block cadence and confirmation depth) and on dev-mode vs real proving; re-run the bench locally to get numbers for your own setup. Canonical numbers will be added once the bench runs against the standard configuration. ## Scenarios @@ -25,7 +16,7 @@ End-to-end LEZ scenarios driven through the wallet against an in-process sequenc ## Dev-mode vs real-proving -`RISC0_DEV_MODE=1` makes the prover emit stub receipts instead of running the recursive STARK pipeline. The table compares each quantity in **dev mode vs real proving** for the two classes of scenarios: +`RISC0_DEV_MODE=1` makes the prover emit stub receipts instead of running the recursive STARK pipeline. The table compares each quantity in dev mode vs real proving for the two classes of scenarios: | Quantity | Public-only scenarios (dev → real) | PPE-bearing scenarios (dev → real) | |---|---|---| @@ -33,71 +24,14 @@ End-to-end LEZ scenarios driven through the wallet against an in-process sequenc | `public_tx_bytes` | same in both modes | same in both modes | | `ppe_tx_bytes` | n/a | dev ≈ 2 KB stub → real ≈ 225 KB (matches `S_agg` from cycle_bench) | | `block_bytes` | same in both modes | real adds ~225 KB per PPE tx in the block | -| `bedrock_finality_ms` | same in both modes | same in both modes (L1 cadence, not LEZ prover) | +| `bedrock_finality_s` | same in both modes | same in both modes (L1 cadence, not LEZ prover) | | Blocks captured | similar in both modes | real captures more empty clock-only ticks that fill prove wall-time | -Tables below report dev-mode for all five scenarios. Real-proving numbers are included for `amm_swap_flow` (representative all-public) and `private_chained_flow` (representative chained-private flow); the public-only scenarios converge between modes within run-to-run jitter, so a full real-proving sweep is not run here. +Numbers are intentionally omitted in this document until the canonical run lands. Public-only scenarios converge between modes within run-to-run jitter; the qualitative differences are captured by the table above. -## Step latencies — dev mode (`RISC0_DEV_MODE=1`) +## Methodology -Per-scenario wall time and Bedrock L1-finality latency for the closing tip. - -| Scenario | total_ms | total_s | bedrock_finality_ms | bedrock_finality_s | -|---|---:|---:|---:|---:| -| token_onboarding | 60,808 | 60.81 | 24,593 | 24.59 | -| amm_swap_flow | 162,058 | 162.06 | 19,210 | 19.21 | -| multi_recipient_fanout | 222,206 | 222.21 | 16,020 | 16.02 | -| private_chained_flow | 80,700 | 80.70 | 23,963 | 23.96 | -| parallel_fanout | 244,387 | 244.39 | 23,770 | 23.77 | - -Total dev-mode wall time across all five: 912.9 s. - -## Step latencies — real proving (selected scenarios) - -| Scenario | total_ms | total_s | bedrock_finality_ms | bedrock_finality_s | Δ vs dev | -|---|---:|---:|---:|---:|---:| -| amm_swap_flow | 162,437 | 162.44 | ~19,210 | ~19.21 | ~0 (all-public) | -| private_chained_flow | 354,843 | 354.84 | 23,778 | 23.78 | +274.14 s (≈ 91 s per PPE step × 3) | - -Per-step breakdown for `private_chained_flow` in real proving: - -| Step | submit_ms | inclusion_ms | total_ms | total_s | -|---|---:|---:|---:|---:| -| token_new_fungible (public) | 1.1 | 20,276.0 | 20,291.2 | 20.29 | -| shielded_transfer (PPE) | 111,683.3 | 1.0 | 111,730.4 | 111.73 | -| deshielded_transfer (PPE) | 111,454.7 | 1.1 | 111,511.2 | 111.51 | -| private_to_private (PPE) | 111,237.0 | 1.1 | 111,293.0 | 111.29 | - -PPE steps move the cost from `inclusion_ms` (waiting for the next sealed block) to `submit_ms` (the wallet itself proving the PPE circuit before sending). Each PPE prove is ≈ 111 s on this CPU. - -## Block + tx sizes (borsh) — dev mode - -Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. The empty clock-only ticks at `min` give the per-block fixed-cost baseline (≈ 334 bytes across all scenarios). - -| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | -|---|---:|---:|---|---:|---:| -| token_onboarding | 6 | 881 | 334..2,890 | 206 / 8 | 2,556 / 1 | -| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | -| multi_recipient_fanout | 22 | 513 | 334..707 | 221 / 33 | n/a | -| private_chained_flow | 8 | 1,399 | 334..3,565 | 177 / 9 | 2,715 / 3 | -| parallel_fanout | 24 | 646 | 334..3,904 | 248 / 45 | n/a | - -## Block + tx sizes (borsh) — real proving - -| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | -|---|---:|---:|---|---:|---:| -| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | -| private_chained_flow | 35 | 19,692 | 334..226,578 | 159 / 36 | 225,728 / 3 | - -`amm_swap_flow` is byte-identical between dev and real (no proof payload). `private_chained_flow`'s `ppe_tx_bytes` matches the cycle_bench `S_agg` measurement (≈ 225 KB borsh InnerReceipt). The `block_bytes` max (226,578) is the block containing the largest PPE transaction. - -## Findings - -- Public-only scenarios converge between dev mode and real proving in both latency and byte counts. Either mode is suitable to characterize them. -- PPE transactions are ≈ 225 KB on the wire in real proving, dominated by the outer succinct proof. Dev mode emits a ≈ 2 KB stub that does not represent the L1 payload — fee-model storage gas inputs must come from a real-proving run. -- Per-PPE-step prove cost on M2 Pro CPU is ≈ 110-120 s, paid on the wallet side at submit time (not on the sequencer). For a single-program chained flow the cost stacks linearly. -- Empty clock-only ticks set the per-block fixed-cost baseline at ≈ 334 bytes across all scenarios and both modes. -- Bedrock L1 finality stays around 20 s regardless of proving mode, because finality is paced by L1 cadence, not the LEZ prover. +Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. Empty clock-only ticks give the per-block fixed-cost baseline. Wall time is captured per step (submit + inclusion + wallet sync) and per scenario (setup + steps + closing bedrock finality wait). ## Reproduce @@ -122,4 +56,5 @@ JSON output: `target/e2e_bench_dev.json` / `target/e2e_bench_prove.json` (suffix - Dev-mode `ppe_tx_bytes` and PPE-step latencies are not representative of production; use real-proving numbers for any fee-model input that touches the storage or prover-cost components. - Single-host run, no GPU acceleration. Real-proving on production prover hardware will move per-step latencies by orders of magnitude; byte counts will not change. - Bedrock running locally; no real network latency between sequencer and Bedrock. +- Bedrock L1 finality (`bedrock_finality_s`) is set by the bedrock config in `LEZ_BEDROCK_CONFIG_DIR` (block cadence × confirmation depth). Different configs will shift `bedrock_finality_s` materially. - Some scenarios share account state via the same wallet; this is intentional (mirrors `integration_tests::TestContext`) and not a realistic multi-wallet workload. From 619db3846d502cec49c7940678c829177a88cb59 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 19 May 2026 22:59:02 +0200 Subject: [PATCH 04/11] refactor(e2e_bench)!: Duration-typed timings, seconds-float JSON, tokio::timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: bench JSON renames per-step / per-scenario timing fields from *_ms (float milliseconds) to *_s (float seconds). Renames: submit_ms → submit_s, inclusion_ms → inclusion_s, wallet_sync_ms → wallet_sync_s, total_ms → total_s, setup_ms → setup_s, bedrock_finality_ms → bedrock_finality_s, total_wall_seconds → total_wall_s. measure_bedrock_finality timeout floor also shifts slightly: on timeout the field is now ~60.000s rather than "first poll tick past 60s". --- Cargo.lock | 4 - tools/e2e_bench/Cargo.toml | 18 +-- tools/e2e_bench/README.md | 2 +- tools/e2e_bench/src/bedrock_handle.rs | 11 +- tools/e2e_bench/src/bench_context.rs | 9 +- tools/e2e_bench/src/harness.rs | 143 +++++++++++--------- tools/e2e_bench/src/main.rs | 151 ++++++++++------------ tools/e2e_bench/src/scenarios/parallel.rs | 29 +++-- 8 files changed, 194 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96afcef6..f238fc2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2382,7 +2382,6 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" name = "e2e_bench" version = "0.1.0" dependencies = [ - "amm_core", "anyhow", "borsh", "chrono", @@ -2392,15 +2391,12 @@ dependencies = [ "indexer_service_rpc", "integration_tests", "jsonrpsee", - "log", "nssa", - "nssa_core", "sequencer_service", "sequencer_service_rpc", "serde", "serde_json", "tempfile", - "token_core", "tokio", "wallet", ] diff --git a/tools/e2e_bench/Cargo.toml b/tools/e2e_bench/Cargo.toml index c6658a8e..ab6a6eb0 100644 --- a/tools/e2e_bench/Cargo.toml +++ b/tools/e2e_bench/Cargo.toml @@ -9,21 +9,21 @@ publish = false workspace = true [dependencies] +common.workspace = true +indexer_service.workspace = true +indexer_service_rpc = { workspace = true, features = ["client"] } integration_tests.workspace = true -wallet.workspace = true nssa.workspace = true sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } -indexer_service.workspace = true -indexer_service_rpc = { workspace = true, features = ["client"] } -jsonrpsee = { workspace = true, features = ["ws-client"] } -common.workspace = true -tempfile.workspace = true -borsh.workspace = true -chrono.workspace = true +wallet.workspace = true anyhow.workspace = true -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } +borsh.workspace = true +chrono.workspace = true clap.workspace = true +jsonrpsee = { workspace = true, features = ["ws-client"] } serde.workspace = true serde_json.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } diff --git a/tools/e2e_bench/README.md b/tools/e2e_bench/README.md index 37d6b175..ddc76bb7 100644 --- a/tools/e2e_bench/README.md +++ b/tools/e2e_bench/README.md @@ -26,7 +26,7 @@ Scenarios: `token`, `amm`, `fanout`, `private`, `parallel`, `all`. ## What you'll see -Per scenario: a step table (`submit_ms`, `inclusion_ms`, `sync_ms`, `total_ms`) and a size summary covering every block captured during the scenario (block_bytes total/mean/min/max; per-tx-variant sizes for public, PPE, and program-deployment transactions). +Per scenario: a step table (`submit_s`, `inclusion_s`, `sync_s`, `total_s`) and a size summary covering every block captured during the scenario (block_bytes total/mean/min/max; per-tx-variant sizes for public, PPE, and program-deployment transactions). The fanout, parallel, and private scenarios are the most representative for L1-payload-size measurements since they put multiple txs per block. diff --git a/tools/e2e_bench/src/bedrock_handle.rs b/tools/e2e_bench/src/bedrock_handle.rs index ef59d8ad..94a8514a 100644 --- a/tools/e2e_bench/src/bedrock_handle.rs +++ b/tools/e2e_bench/src/bedrock_handle.rs @@ -2,14 +2,19 @@ //! Launches a fresh Bedrock instance per scenario so the indexer never has to //! catch up a large finalization backlog. //! -//! Required env vars (no defaults — path layouts differ per developer): -//! - `LEZ_BEDROCK_BIN` — absolute path to the `logos-blockchain-node` binary. -//! - `LEZ_BEDROCK_CONFIG_DIR` — directory containing `node-config.yaml` and +//! Required env vars (no defaults, path layouts differ per developer): +//! - `LEZ_BEDROCK_BIN` absolute path to the `logos-blockchain-node` binary. +//! - `LEZ_BEDROCK_CONFIG_DIR` directory containing `node-config.yaml` and //! `deployment-settings.yaml` (template with `PLACEHOLDER_CHAIN_START_TIME`). //! //! Optional: //! - `LEZ_BEDROCK_PORT` (default: 18080) +#![allow( + clippy::let_underscore_must_use, + reason = "file is deleted in the docker-compose pivot; teardown ignores child kill/wait results by design" +)] + use std::{ env, net::SocketAddr, diff --git a/tools/e2e_bench/src/bench_context.rs b/tools/e2e_bench/src/bench_context.rs index 43376811..41f0d59d 100644 --- a/tools/e2e_bench/src/bench_context.rs +++ b/tools/e2e_bench/src/bench_context.rs @@ -1,4 +1,4 @@ -//! BenchContext: wires sequencer + indexer + wallet in-process against an +//! `BenchContext`: wires sequencer + indexer + wallet in-process against an //! externally-running Bedrock node. Mirrors the surface of //! `integration_tests::TestContext` for the methods the scenarios need //! (`wallet_mut()`, `sequencer_client()`), but skips the docker setup. @@ -6,6 +6,11 @@ //! The external Bedrock URL defaults to 127.0.0.1:18080 and can be overridden //! with the `LEZ_BEDROCK_ADDR` env var. +#![allow( + clippy::arbitrary_source_item_ordering, + reason = "file is deleted in the docker-compose pivot; ordering churn is wasted work" +)] + use std::{env, net::SocketAddr, path::Path}; use anyhow::{Context as _, Result}; @@ -145,7 +150,7 @@ impl BenchContext { &self.sequencer_client } - pub fn indexer_addr(&self) -> SocketAddr { + pub const fn indexer_addr(&self) -> SocketAddr { self.indexer_handle.addr() } diff --git a/tools/e2e_bench/src/harness.rs b/tools/e2e_bench/src/harness.rs index bcdff7a7..c83904fd 100644 --- a/tools/e2e_bench/src/harness.rs +++ b/tools/e2e_bench/src/harness.rs @@ -1,11 +1,16 @@ //! Step / scenario timing primitives shared across scenarios. +#![allow( + clippy::ref_option, + reason = "serde::serialize_with requires fn(&Option, S) -> Result<...>" +)] + use std::time::{Duration, Instant}; use anyhow::{Result, bail}; use common::transaction::NSSATransaction; use sequencer_service_rpc::RpcClient as _; -use serde::Serialize; +use serde::{Serialize, Serializer}; use wallet::cli::SubcommandReturnValue; use crate::bench_context::BenchContext; @@ -16,7 +21,7 @@ const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120); /// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes` /// is the full Block (header + body + bedrock metadata) and is the closest /// proxy we have to the L1 payload posted per block. `tx_bytes` is each contained -/// transaction split by variant — this is what the fee model's S_tx slot covers. +/// transaction split by variant, which is what the fee model's `S_tx` slot covers. #[derive(Debug, Serialize, Clone, Default)] pub struct BlockSize { pub block_id: u64, @@ -29,22 +34,28 @@ pub struct BlockSize { #[derive(Debug, Serialize, Clone)] pub struct StepResult { pub label: String, - pub submit_ms: f64, - pub inclusion_ms: Option, - pub wallet_sync_ms: Option, - pub total_ms: f64, + #[serde(serialize_with = "ser_duration_secs", rename = "submit_s")] + pub submit: Duration, + #[serde(serialize_with = "ser_opt_duration_secs", rename = "inclusion_s")] + pub inclusion: Option, + #[serde(serialize_with = "ser_opt_duration_secs", rename = "wallet_sync_s")] + pub wallet_sync: Option, + #[serde(serialize_with = "ser_duration_secs", rename = "total_s")] + pub total: Duration, pub tx_hash: Option, /// Borsh sizes for every zone block produced during this step. - /// Empty for steps that don't advance the chain (e.g. RegisterAccount). + /// Empty for steps that don't advance the chain (e.g. `RegisterAccount`). pub blocks: Vec, } #[derive(Debug, Serialize, Default)] pub struct ScenarioResult { pub name: String, - pub setup_ms: f64, + #[serde(serialize_with = "ser_duration_secs", rename = "setup_s")] + pub setup: Duration, pub steps: Vec, - pub total_ms: f64, + #[serde(serialize_with = "ser_duration_secs", rename = "total_s")] + pub total: Duration, /// Disk sizes (sequencer / indexer / wallet tempdirs) sampled at scenario start. pub disk_before: Option, /// Disk sizes sampled at scenario end. @@ -53,7 +64,8 @@ pub struct ScenarioResult { /// reporting the sequencer tip as L1-finalised. Effectively measures the /// sequencer→Bedrock posting + Bedrock finalisation + indexer L1 ingest path. /// A value at the timeout (60s) means finalisation did not happen within the bench window. - pub bedrock_finality_ms: Option, + #[serde(serialize_with = "ser_opt_duration_secs", rename = "bedrock_finality_s")] + pub bedrock_finality: Option, } impl ScenarioResult { @@ -65,11 +77,18 @@ impl ScenarioResult { } pub fn push(&mut self, step: StepResult) { - self.total_ms += step.total_ms; + self.total = self.total.saturating_add(step.total); self.steps.push(step); } } +/// Begin a timed step. Capture this *before* submitting the wallet operation +/// so we can later subtract it from the post-submit block height to detect +/// when the chain has advanced past the tx's block. +pub async fn begin_step(ctx: &BenchContext) -> Result { + Ok(ctx.sequencer_client().get_last_block_id().await?) +} + /// Finish a timed wallet step. Records submit (the time between `started` /// being captured and `ret` being received) and, if `ret` is a /// [`SubcommandReturnValue::PrivacyPreservingTransfer`], polls the sequencer @@ -79,15 +98,8 @@ impl ScenarioResult { /// ```ignore /// let started = Instant::now(); /// let ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), cmd).await?; -/// let step = finalize_step("label", started, ret, ctx).await?; +/// let step = finalize_step("label", started, pre_block_id, &ret, ctx).await?; /// ``` -/// Begin a timed step. Capture this *before* submitting the wallet operation -/// so we can later subtract it from the post-submit block height to detect -/// when the chain has advanced past the tx's block. -pub async fn begin_step(ctx: &BenchContext) -> Result { - Ok(ctx.sequencer_client().get_last_block_id().await?) -} - pub async fn finalize_step( label: impl Into, started: Instant, @@ -96,11 +108,11 @@ pub async fn finalize_step( ctx: &mut BenchContext, ) -> Result { let label = label.into(); - let submit_ms = started.elapsed().as_secs_f64() * 1_000.0; + let submit = started.elapsed(); let mut tx_hash_str = None; - let mut inclusion_ms = None; - let mut wallet_sync_ms = None; + let mut inclusion = None; + let mut wallet_sync = None; let mut blocks: Vec = Vec::new(); // For non-account-create steps (anything that produces a tx_hash, or even @@ -115,11 +127,11 @@ pub async fn finalize_step( } let started_inclusion = Instant::now(); wait_for_chain_advance(ctx, pre_block_id, 2).await?; - inclusion_ms = Some(started_inclusion.elapsed().as_secs_f64() * 1_000.0); + inclusion = Some(started_inclusion.elapsed()); let started_sync = Instant::now(); sync_wallet_to_tip(ctx).await?; - wallet_sync_ms = Some(started_sync.elapsed().as_secs_f64() * 1_000.0); + wallet_sync = Some(started_sync.elapsed()); // Capture block-byte and per-tx-byte sizes for every block produced // during this step. We intentionally capture all blocks, including @@ -151,10 +163,10 @@ pub async fn finalize_step( Ok(StepResult { label, - submit_ms, - inclusion_ms, - wallet_sync_ms, - total_ms: started.elapsed().as_secs_f64() * 1_000.0, + submit, + inclusion, + wallet_sync, + total: started.elapsed(), tx_hash: tx_hash_str, blocks, }) @@ -167,19 +179,21 @@ pub async fn wait_for_chain_advance( min_blocks: u64, ) -> Result<()> { let target = from_block_id.saturating_add(min_blocks); - let deadline = Instant::now() + TX_INCLUSION_TIMEOUT; - loop { - match ctx.sequencer_client().get_last_block_id().await { - Ok(current) if current >= target => return Ok(()), - Ok(_) => {} - Err(err) => eprintln!("get_last_block_id error (continuing poll): {err:#}"), + let poll = async { + loop { + match ctx.sequencer_client().get_last_block_id().await { + Ok(current) if current >= target => return, + Ok(_) => {} + Err(err) => eprintln!("get_last_block_id error (continuing poll): {err:#}"), + } + tokio::time::sleep(TX_INCLUSION_POLL_INTERVAL).await; } - if Instant::now() > deadline { - bail!( - "chain did not advance from {from_block_id} to at least {target} within {TX_INCLUSION_TIMEOUT:?}" - ); - } - tokio::time::sleep(TX_INCLUSION_POLL_INTERVAL).await; + }; + match tokio::time::timeout(TX_INCLUSION_TIMEOUT, poll).await { + Ok(()) => Ok(()), + Err(_) => bail!( + "chain did not advance from {from_block_id} to at least {target} within {TX_INCLUSION_TIMEOUT:?}" + ), } } @@ -199,38 +213,35 @@ pub fn print_table(result: &ScenarioResult) { .max("step".len()); println!( - "\nScenario: {} (setup {:.1} ms ({:.2}s), total {:.1} ms ({:.2}s))", + "\nScenario: {} (setup {:.2}s, total {:.2}s)", result.name, - result.setup_ms, - result.setup_ms / 1_000.0, - result.total_ms, - result.total_ms / 1_000.0, + result.setup.as_secs_f64(), + result.total.as_secs_f64(), ); println!( - "{:10} {:>12} {:>10} {:>16}", + "{:10} {:>12} {:>10} {:>10}", "step", - "submit_ms", - "inclusion_ms", - "sync_ms", - "total_ms (s)", + "submit_s", + "inclusion_s", + "sync_s", + "total_s", lw = label_width, ); - println!("{}", "-".repeat(label_width + 62)); + println!("{}", "-".repeat(label_width.saturating_add(50))); for s in &result.steps { let inclusion = s - .inclusion_ms - .map_or_else(|| "-".to_owned(), |v| format!("{v:.1}")); + .inclusion + .map_or_else(|| "-".to_owned(), |v| format!("{:.3}", v.as_secs_f64())); let sync = s - .wallet_sync_ms - .map_or_else(|| "-".to_owned(), |v| format!("{v:.1}")); - let total = format!("{:.1} ({:.2}s)", s.total_ms, s.total_ms / 1_000.0); + .wallet_sync + .map_or_else(|| "-".to_owned(), |v| format!("{:.3}", v.as_secs_f64())); println!( - "{:10.1} {:>12} {:>10} {:>16}", + "{:10.3} {:>12} {:>10} {:>10.3}", s.label, - s.submit_ms, + s.submit.as_secs_f64(), inclusion, sync, - total, + s.total.as_secs_f64(), lw = label_width, ); } @@ -295,3 +306,17 @@ fn print_tx_line(label: &str, samples: &[usize]) { fn mean_usize(xs: &[usize]) -> usize { xs.iter().sum::().checked_div(xs.len()).unwrap_or(0) } + +fn ser_duration_secs(d: &Duration, s: S) -> std::result::Result { + s.serialize_f64(d.as_secs_f64()) +} + +fn ser_opt_duration_secs( + d: &Option, + s: S, +) -> std::result::Result { + match d { + Some(d) => s.serialize_f64(d.as_secs_f64()), + None => s.serialize_none(), + } +} diff --git a/tools/e2e_bench/src/main.rs b/tools/e2e_bench/src/main.rs index 31ea1189..80f547ab 100644 --- a/tools/e2e_bench/src/main.rs +++ b/tools/e2e_bench/src/main.rs @@ -7,34 +7,25 @@ //! block + tx sizes per scenario. //! //! Required env vars (no defaults; see `tools/e2e_bench/README.md`): -//! LEZ_BEDROCK_BIN absolute path to logos-blockchain-node. -//! LEZ_BEDROCK_CONFIG_DIR directory with node-config.yaml + deployment template. +//! `LEZ_BEDROCK_BIN` absolute path to logos-blockchain-node. +//! `LEZ_BEDROCK_CONFIG_DIR` directory with node-config.yaml + deployment template. //! //! Run examples: -//! RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all. -//! cargo run --release -p e2e_bench -- --scenario amm. +//! `RISC0_DEV_MODE=1` `cargo run --release -p e2e_bench -- --scenario all`. +//! `cargo run --release -p e2e_bench -- --scenario amm`. //! //! `RISC0_DEV_MODE=1` skips proving and produces latency-only numbers in //! ~minutes; omitting it produces realistic proving-inclusive numbers but //! the run takes much longer. -#![expect( - clippy::arbitrary_source_item_ordering, +#![allow( clippy::arithmetic_side_effects, - clippy::as_conversions, - clippy::doc_markdown, - clippy::float_arithmetic, - clippy::let_underscore_must_use, - clippy::let_underscore_untyped, - clippy::missing_const_for_fn, clippy::print_stderr, clippy::print_stdout, - clippy::single_call_fn, - clippy::single_match_else, - clippy::std_instead_of_core, - clippy::too_many_lines, clippy::wildcard_enum_match_arm, - reason = "Bench tool: matches test-style fixture code" + reason = "Bench tool: stderr/stdout output is the deliverable; small Duration / iterator-sum \ + arithmetic is safe at bench scale; bench scenarios bail loudly on any unexpected \ + return variant, which is preferable to maintaining an exhaustive list in five files." )] use std::{path::PathBuf, time::Duration}; @@ -68,7 +59,7 @@ struct Cli { #[arg(long, value_enum, default_value_t = ScenarioName::All)] scenario: ScenarioName, - /// Optional JSON output path. Defaults to /target/e2e_bench.json. + /// Optional JSON output path. Defaults to `/target/e2e_bench.json`. #[arg(long)] json_out: Option, } @@ -77,7 +68,7 @@ struct Cli { struct BenchRunReport { risc0_dev_mode: bool, scenarios: Vec, - total_wall_seconds: f64, + total_wall_s: f64, } #[tokio::main(flavor = "multi_thread")] @@ -110,61 +101,61 @@ async fn main() -> Result<()> { for name in to_run { eprintln!("\n=== running scenario: {name:?} ==="); - let setup_started = std::time::Instant::now(); - // Spawn a fresh Bedrock node for this scenario. Each scenario therefore - // starts with an empty chain so the indexer never has a backlog from a - // prior scenario. - let bedrock = BedrockHandle::launch_fresh() - .await - .with_context(|| format!("failed to spawn Bedrock for scenario {name:?}"))?; - let bedrock_addr_string = format!("{}", bedrock.addr()); - // Safety: we restore the previous LEZ_BEDROCK_ADDR value (if any) at scenario teardown. - // SAFETY: this happens before any threaded setup that reads env. - unsafe { - std::env::set_var("LEZ_BEDROCK_ADDR", &bedrock_addr_string); + { + let setup_started = std::time::Instant::now(); + // Spawn a fresh Bedrock node for this scenario. Each scenario therefore + // starts with an empty chain so the indexer never has a backlog from a + // prior scenario. + let bedrock = BedrockHandle::launch_fresh() + .await + .with_context(|| format!("failed to spawn Bedrock for scenario {name:?}"))?; + let bedrock_addr_string = format!("{}", bedrock.addr()); + // SAFETY: env::set_var happens before any threaded setup that reads env. + unsafe { + std::env::set_var("LEZ_BEDROCK_ADDR", &bedrock_addr_string); + } + + let mut ctx = BenchContext::new() + .await + .with_context(|| format!("failed to setup BenchContext for scenario {name:?}"))?; + let setup = setup_started.elapsed(); + eprintln!("setup: {:.2}s", setup.as_secs_f64()); + + let disk_before = ctx.disk_sizes(); + let mut result = run_scenario(name, setup, &mut ctx).await?; + result.disk_before = Some(disk_before); + result.disk_after = Some(ctx.disk_sizes()); + result.bedrock_finality = Some(measure_bedrock_finality(&ctx).await?); + harness::print_table(&result); + all_results.push(result); + + // ctx and bedrock drop here at end of scope, killing the bedrock child + // before we sleep so the next iteration can rebind the port. } - - let mut ctx = BenchContext::new() - .await - .with_context(|| format!("failed to setup BenchContext for scenario {name:?}"))?; - let setup_ms = elapsed_ms(setup_started); - eprintln!("setup: {setup_ms:.1} ms"); - - let disk_before = ctx.disk_sizes(); - let mut result = run_scenario(name, setup_ms, &mut ctx).await?; - result.disk_before = Some(disk_before); - result.disk_after = Some(ctx.disk_sizes()); - result.bedrock_finality_ms = Some(measure_bedrock_finality(&ctx).await?); - harness::print_table(&result); - all_results.push(result); - - drop(ctx); - drop(bedrock); // Give Bedrock a moment to shut down before the next scenario. tokio::time::sleep(Duration::from_secs(2)).await; } - let total_wall_seconds = overall_started.elapsed().as_secs_f64(); - eprintln!("\nTotal wall time: {total_wall_seconds:.1}s"); + let total_wall_s = overall_started.elapsed().as_secs_f64(); + eprintln!("\nTotal wall time: {total_wall_s:.1}s"); let report = BenchRunReport { risc0_dev_mode, scenarios: all_results, - total_wall_seconds, + total_wall_s, }; - let out_path = match cli.json_out { - Some(p) => p, - None => { - let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .canonicalize()?; - let suffix = if risc0_dev_mode { "dev" } else { "prove" }; - workspace_root - .join("target") - .join(format!("e2e_bench_{suffix}.json")) - } + let out_path = if let Some(p) = cli.json_out { + p + } else { + let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .canonicalize()?; + let suffix = if risc0_dev_mode { "dev" } else { "prove" }; + workspace_root + .join("target") + .join(format!("e2e_bench_{suffix}.json")) }; if let Some(parent) = out_path.parent() { std::fs::create_dir_all(parent)?; @@ -177,7 +168,7 @@ async fn main() -> Result<()> { async fn run_scenario( name: ScenarioName, - setup_ms: f64, + setup: Duration, ctx: &mut BenchContext, ) -> Result { let result = match name { @@ -188,17 +179,13 @@ async fn run_scenario( ScenarioName::Parallel => scenarios::parallel::run(ctx).await?, ScenarioName::All => unreachable!("dispatched above"), }; - Ok(ScenarioResult { setup_ms, ..result }) -} - -fn elapsed_ms(t: std::time::Instant) -> f64 { - t.elapsed().as_secs_f64() * 1_000.0 + Ok(ScenarioResult { setup, ..result }) } /// Poll the indexer's L1-finalised block id until it catches up with the /// sequencer's last block id. This is effectively the sequencer→Bedrock posting /// plus Bedrock finalisation plus indexer ingest latency. -async fn measure_bedrock_finality(ctx: &BenchContext) -> Result { +async fn measure_bedrock_finality(ctx: &BenchContext) -> Result { use indexer_service_rpc::RpcClient as _; use jsonrpsee::ws_client::WsClientBuilder; use sequencer_service_rpc::RpcClient as _; @@ -210,20 +197,20 @@ async fn measure_bedrock_finality(ctx: &BenchContext) -> Result { .context("connect indexer WS")?; let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?; + let timeout = Duration::from_secs(60); let started = std::time::Instant::now(); - let deadline = started + Duration::from_secs(60); - loop { - match indexer_ws.get_last_finalized_block_id().await { - Ok(Some(b)) if b >= sequencer_tip => { - return Ok(started.elapsed().as_secs_f64() * 1_000.0); + let poll = async { + loop { + match indexer_ws.get_last_finalized_block_id().await { + Ok(Some(b)) if b >= sequencer_tip => return, + Ok(_) => {} + Err(err) => eprintln!("indexer last_synced poll error: {err:#}"), } - Ok(_) => {} - Err(err) => eprintln!("indexer last_synced poll error: {err:#}"), + tokio::time::sleep(Duration::from_millis(200)).await; } - if std::time::Instant::now() > deadline { - eprintln!("indexer did not catch up to {sequencer_tip} within 60s"); - return Ok(started.elapsed().as_secs_f64() * 1_000.0); - } - tokio::time::sleep(Duration::from_millis(200)).await; + }; + if tokio::time::timeout(timeout, poll).await.is_err() { + eprintln!("indexer did not catch up to {sequencer_tip} within {timeout:?}"); } + Ok(started.elapsed()) } diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/e2e_bench/src/scenarios/parallel.rs index 43bd25ac..23dd2247 100644 --- a/tools/e2e_bench/src/scenarios/parallel.rs +++ b/tools/e2e_bench/src/scenarios/parallel.rs @@ -42,7 +42,10 @@ pub async fn run(ctx: &mut BenchContext) -> Result { } // Mint full supply into master. - let total_mint: u128 = (PARALLEL_FANOUT_N as u128) * AMOUNT_PER_TRANSFER * 10; + let total_mint = u128::try_from(PARALLEL_FANOUT_N) + .expect("usize fits u128") + .saturating_mul(AMOUNT_PER_TRANSFER) + .saturating_mul(10); { let pre_block = crate::harness::begin_step(ctx).await?; let started = Instant::now(); @@ -104,21 +107,21 @@ pub async fn run(ctx: &mut BenchContext) -> Result { .await?; } let all_submitted_at = Instant::now(); - let submit_duration_ms = (all_submitted_at - burst_started).as_secs_f64() * 1_000.0; + let submit_duration = all_submitted_at.saturating_duration_since(burst_started); // Wait for the chain to advance by at least 2 blocks past pre_block_burst. // That guarantees the block holding our burst is sealed and applied. crate::harness::wait_for_chain_advance(ctx, pre_block_burst, 2).await?; let inclusion_done_at = Instant::now(); - let inclusion_after_submit_ms = (inclusion_done_at - all_submitted_at).as_secs_f64() * 1_000.0; - let burst_total_ms = (inclusion_done_at - burst_started).as_secs_f64() * 1_000.0; + let inclusion_after_submit = inclusion_done_at.saturating_duration_since(all_submitted_at); + let burst_total = inclusion_done_at.saturating_duration_since(burst_started); eprintln!( - "parallel_fanout: submitted {} txs in {:.1} ms, inclusion in {:.1} ms, total {:.1} ms", + "parallel_fanout: submitted {} txs in {:.3}s, inclusion in {:.3}s, total {:.3}s", senders.len(), - submit_duration_ms, - inclusion_after_submit_ms, - burst_total_ms, + submit_duration.as_secs_f64(), + inclusion_after_submit.as_secs_f64(), + burst_total.as_secs_f64(), ); // Capture every block produced during the burst window. This is the @@ -149,13 +152,13 @@ pub async fn run(ctx: &mut BenchContext) -> Result { } // Synthesise a single summary "step" for the burst. Use the submit time - // for `submit_ms` and the inclusion-wait time for `inclusion_ms`. + // for `submit` and the inclusion-wait time for `inclusion`. let burst_step = StepResult { label: format!("burst_{}_transfers", senders.len()), - submit_ms: submit_duration_ms, - inclusion_ms: Some(inclusion_after_submit_ms), - wallet_sync_ms: None, - total_ms: burst_total_ms, + submit: submit_duration, + inclusion: Some(inclusion_after_submit), + wallet_sync: None, + total: burst_total, tx_hash: None, blocks, }; From 932763fcf2a5a98770c2a046c2cbc14916414f6f Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 19 May 2026 23:48:05 +0200 Subject: [PATCH 05/11] refactor(e2e_bench): rename ScenarioResult to ScenarioOutput --- tools/e2e_bench/src/harness.rs | 22 +++++----- tools/e2e_bench/src/main.rs | 26 ++++++------ tools/e2e_bench/src/scenarios/amm.rs | 50 +++++++++++------------ tools/e2e_bench/src/scenarios/fanout.rs | 22 +++++----- tools/e2e_bench/src/scenarios/parallel.rs | 26 ++++++------ tools/e2e_bench/src/scenarios/private.rs | 34 +++++++-------- tools/e2e_bench/src/scenarios/token.rs | 30 +++++++------- 7 files changed, 105 insertions(+), 105 deletions(-) diff --git a/tools/e2e_bench/src/harness.rs b/tools/e2e_bench/src/harness.rs index c83904fd..7020352b 100644 --- a/tools/e2e_bench/src/harness.rs +++ b/tools/e2e_bench/src/harness.rs @@ -49,7 +49,7 @@ pub struct StepResult { } #[derive(Debug, Serialize, Default)] -pub struct ScenarioResult { +pub struct ScenarioOutput { pub name: String, #[serde(serialize_with = "ser_duration_secs", rename = "setup_s")] pub setup: Duration, @@ -68,7 +68,7 @@ pub struct ScenarioResult { pub bedrock_finality: Option, } -impl ScenarioResult { +impl ScenarioOutput { pub fn new(name: impl Into) -> Self { Self { name: name.into(), @@ -203,8 +203,8 @@ async fn sync_wallet_to_tip(ctx: &mut BenchContext) -> Result<()> { Ok(()) } -pub fn print_table(result: &ScenarioResult) { - let label_width = result +pub fn print_table(output: &ScenarioOutput) { + let label_width = output .steps .iter() .map(|s| s.label.len()) @@ -214,9 +214,9 @@ pub fn print_table(result: &ScenarioResult) { println!( "\nScenario: {} (setup {:.2}s, total {:.2}s)", - result.name, - result.setup.as_secs_f64(), - result.total.as_secs_f64(), + output.name, + output.setup.as_secs_f64(), + output.total.as_secs_f64(), ); println!( "{:10} {:>12} {:>10} {:>10}", @@ -228,7 +228,7 @@ pub fn print_table(result: &ScenarioResult) { lw = label_width, ); println!("{}", "-".repeat(label_width.saturating_add(50))); - for s in &result.steps { + for s in &output.steps { let inclusion = s .inclusion .map_or_else(|| "-".to_owned(), |v| format!("{:.3}", v.as_secs_f64())); @@ -246,13 +246,13 @@ pub fn print_table(result: &ScenarioResult) { ); } - print_size_summary(result); + print_size_summary(output); } /// Aggregate borsh sizes per scenario: total/mean/min/max block bytes, and /// per-tx bytes split by variant. Empty if no blocks were captured. -fn print_size_summary(result: &ScenarioResult) { - let blocks: Vec<&BlockSize> = result.steps.iter().flat_map(|s| s.blocks.iter()).collect(); +fn print_size_summary(output: &ScenarioOutput) { + let blocks: Vec<&BlockSize> = output.steps.iter().flat_map(|s| s.blocks.iter()).collect(); if blocks.is_empty() { return; } diff --git a/tools/e2e_bench/src/main.rs b/tools/e2e_bench/src/main.rs index 80f547ab..7de1d323 100644 --- a/tools/e2e_bench/src/main.rs +++ b/tools/e2e_bench/src/main.rs @@ -34,7 +34,7 @@ use anyhow::{Context as _, Result}; use bedrock_handle::BedrockHandle; use bench_context::BenchContext; use clap::{Parser, ValueEnum}; -use harness::ScenarioResult; +use harness::ScenarioOutput; use serde::Serialize; mod bedrock_handle; @@ -67,7 +67,7 @@ struct Cli { #[derive(Debug, Serialize)] struct BenchRunReport { risc0_dev_mode: bool, - scenarios: Vec, + scenarios: Vec, total_wall_s: f64, } @@ -97,7 +97,7 @@ async fn main() -> Result<()> { }; let overall_started = std::time::Instant::now(); - let mut all_results = Vec::with_capacity(to_run.len()); + let mut all_outputs = Vec::with_capacity(to_run.len()); for name in to_run { eprintln!("\n=== running scenario: {name:?} ==="); @@ -122,12 +122,12 @@ async fn main() -> Result<()> { eprintln!("setup: {:.2}s", setup.as_secs_f64()); let disk_before = ctx.disk_sizes(); - let mut result = run_scenario(name, setup, &mut ctx).await?; - result.disk_before = Some(disk_before); - result.disk_after = Some(ctx.disk_sizes()); - result.bedrock_finality = Some(measure_bedrock_finality(&ctx).await?); - harness::print_table(&result); - all_results.push(result); + let mut output = run_scenario(name, setup, &mut ctx).await?; + output.disk_before = Some(disk_before); + output.disk_after = Some(ctx.disk_sizes()); + output.bedrock_finality = Some(measure_bedrock_finality(&ctx).await?); + harness::print_table(&output); + all_outputs.push(output); // ctx and bedrock drop here at end of scope, killing the bedrock child // before we sleep so the next iteration can rebind the port. @@ -141,7 +141,7 @@ async fn main() -> Result<()> { let report = BenchRunReport { risc0_dev_mode, - scenarios: all_results, + scenarios: all_outputs, total_wall_s, }; @@ -170,8 +170,8 @@ async fn run_scenario( name: ScenarioName, setup: Duration, ctx: &mut BenchContext, -) -> Result { - let result = match name { +) -> Result { + let output = match name { ScenarioName::Token => scenarios::token::run(ctx).await?, ScenarioName::Amm => scenarios::amm::run(ctx).await?, ScenarioName::Fanout => scenarios::fanout::run(ctx).await?, @@ -179,7 +179,7 @@ async fn run_scenario( ScenarioName::Parallel => scenarios::parallel::run(ctx).await?, ScenarioName::All => unreachable!("dispatched above"), }; - Ok(ScenarioResult { setup, ..result }) + Ok(ScenarioOutput { setup, ..output }) } /// Poll the indexer's L1-finalised block id until it catches up with the diff --git a/tools/e2e_bench/src/scenarios/amm.rs b/tools/e2e_bench/src/scenarios/amm.rs index b92bd823..6756321d 100644 --- a/tools/e2e_bench/src/scenarios/amm.rs +++ b/tools/e2e_bench/src/scenarios/amm.rs @@ -10,25 +10,25 @@ use wallet::cli::{ programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, }; -use crate::harness::{ScenarioResult, finalize_step}; +use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { - let mut result = ScenarioResult::new("amm_swap_flow"); +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut output = ScenarioOutput::new("amm_swap_flow"); - let def_a = new_public_account(ctx, &mut result, "create_acc_def_a").await?; - let supply_a = new_public_account(ctx, &mut result, "create_acc_supply_a").await?; - let user_a = new_public_account(ctx, &mut result, "create_acc_user_a").await?; + let def_a = new_public_account(ctx, &mut output, "create_acc_def_a").await?; + let supply_a = new_public_account(ctx, &mut output, "create_acc_supply_a").await?; + let user_a = new_public_account(ctx, &mut output, "create_acc_user_a").await?; - let def_b = new_public_account(ctx, &mut result, "create_acc_def_b").await?; - let supply_b = new_public_account(ctx, &mut result, "create_acc_supply_b").await?; - let user_b = new_public_account(ctx, &mut result, "create_acc_user_b").await?; + let def_b = new_public_account(ctx, &mut output, "create_acc_def_b").await?; + let supply_b = new_public_account(ctx, &mut output, "create_acc_supply_b").await?; + let user_b = new_public_account(ctx, &mut output, "create_acc_user_b").await?; - let user_lp = new_public_account(ctx, &mut result, "create_acc_user_lp").await?; + let user_lp = new_public_account(ctx, &mut output, "create_acc_user_lp").await?; - timed_token_new(ctx, &mut result, "token_a_new", def_a, supply_a, "TokA").await?; + timed_token_new(ctx, &mut output, "token_a_new", def_a, supply_a, "TokA").await?; timed_token_send( ctx, - &mut result, + &mut output, "token_a_fund_user", supply_a, user_a, @@ -36,10 +36,10 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result Result Result Result Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -140,7 +140,7 @@ async fn new_public_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -149,7 +149,7 @@ async fn new_public_account( async fn timed_token_new( ctx: &mut crate::bench_context::BenchContext, - result: &mut ScenarioResult, + output: &mut ScenarioOutput, label: &str, def_id: nssa::AccountId, supply_id: nssa::AccountId, @@ -168,13 +168,13 @@ async fn timed_token_new( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); Ok(()) } async fn timed_token_send( ctx: &mut crate::bench_context::BenchContext, - result: &mut ScenarioResult, + output: &mut ScenarioOutput, label: &str, from_id: nssa::AccountId, to_id: nssa::AccountId, @@ -195,6 +195,6 @@ async fn timed_token_send( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); Ok(()) } diff --git a/tools/e2e_bench/src/scenarios/fanout.rs b/tools/e2e_bench/src/scenarios/fanout.rs index adede185..59e9a64b 100644 --- a/tools/e2e_bench/src/scenarios/fanout.rs +++ b/tools/e2e_bench/src/scenarios/fanout.rs @@ -10,16 +10,16 @@ use wallet::cli::{ programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioResult, finalize_step}; +use crate::harness::{ScenarioOutput, finalize_step}; const FANOUT_COUNT: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { - let mut result = ScenarioResult::new("multi_recipient_fanout"); +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut output = ScenarioOutput::new("multi_recipient_fanout"); - let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; - let supply_id = new_public_account(ctx, &mut result, "create_acc_supply").await?; + let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; + let supply_id = new_public_account(ctx, &mut output, "create_acc_supply").await?; { let pre_block = crate::harness::begin_step(ctx).await?; @@ -35,12 +35,12 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -82,7 +82,7 @@ async fn new_public_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/e2e_bench/src/scenarios/parallel.rs index 23dd2247..86368a0d 100644 --- a/tools/e2e_bench/src/scenarios/parallel.rs +++ b/tools/e2e_bench/src/scenarios/parallel.rs @@ -17,27 +17,27 @@ use wallet::cli::{ use crate::{ bench_context::BenchContext, - harness::{BlockSize, ScenarioResult, StepResult, finalize_step}, + harness::{BlockSize, ScenarioOutput, StepResult, finalize_step}, }; const PARALLEL_FANOUT_N: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; -pub async fn run(ctx: &mut BenchContext) -> Result { - let mut result = ScenarioResult::new("parallel_fanout"); +pub async fn run(ctx: &mut BenchContext) -> Result { + let mut output = ScenarioOutput::new("parallel_fanout"); // Setup: definition, master supply, N parallel supplies, N recipients. - let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; - let master_id = new_public_account(ctx, &mut result, "create_acc_master").await?; + let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; + let master_id = new_public_account(ctx, &mut output, "create_acc_master").await?; let mut senders = Vec::with_capacity(PARALLEL_FANOUT_N); for i in 0..PARALLEL_FANOUT_N { - let id = new_public_account(ctx, &mut result, &format!("create_sender_{i:02}")).await?; + let id = new_public_account(ctx, &mut output, &format!("create_sender_{i:02}")).await?; senders.push(id); } let mut recipients = Vec::with_capacity(PARALLEL_FANOUT_N); for i in 0..PARALLEL_FANOUT_N { - let id = new_public_account(ctx, &mut result, &format!("create_recipient_{i:02}")).await?; + let id = new_public_account(ctx, &mut output, &format!("create_recipient_{i:02}")).await?; recipients.push(id); } @@ -60,7 +60,7 @@ pub async fn run(ctx: &mut BenchContext) -> Result { ) .await?; let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); } // Fund each sender from master. Serial; this is setup, not measured throughput. @@ -81,7 +81,7 @@ pub async fn run(ctx: &mut BenchContext) -> Result { .await?; let step = finalize_step(format!("fund_sender_{i:02}"), started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); } // The measured phase: submit N transfers as fast as possible, do not wait @@ -162,14 +162,14 @@ pub async fn run(ctx: &mut BenchContext) -> Result { tx_hash: None, blocks, }; - result.push(burst_step); + output.push(burst_step); - Ok(result) + Ok(output) } async fn new_public_account( ctx: &mut BenchContext, - result: &mut ScenarioResult, + output: &mut ScenarioOutput, label: &str, ) -> Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -183,7 +183,7 @@ async fn new_public_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/e2e_bench/src/scenarios/private.rs b/tools/e2e_bench/src/scenarios/private.rs index 2a154673..c6ef9888 100644 --- a/tools/e2e_bench/src/scenarios/private.rs +++ b/tools/e2e_bench/src/scenarios/private.rs @@ -10,17 +10,17 @@ use wallet::cli::{ programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioResult, finalize_step}; +use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { - let mut result = ScenarioResult::new("private_chained_flow"); +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut output = ScenarioOutput::new("private_chained_flow"); - let def_id = new_public_account(ctx, &mut result, "create_acc_def").await?; - let supply_id = new_public_account(ctx, &mut result, "create_acc_supply").await?; + let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; + let supply_id = new_public_account(ctx, &mut output, "create_acc_supply").await?; let public_recipient_id = - new_public_account(ctx, &mut result, "create_acc_pub_recipient").await?; - let private_a = new_private_account(ctx, &mut result, "create_acc_priv_a").await?; - let private_b = new_private_account(ctx, &mut result, "create_acc_priv_b").await?; + new_public_account(ctx, &mut output, "create_acc_pub_recipient").await?; + let private_a = new_private_account(ctx, &mut output, "create_acc_priv_a").await?; + let private_b = new_private_account(ctx, &mut output, "create_acc_priv_b").await?; // Mint into public supply. { @@ -37,7 +37,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result private_a. @@ -57,7 +57,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result public_recipient. @@ -77,7 +77,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result private_b. @@ -97,15 +97,15 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -119,7 +119,7 @@ async fn new_public_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -128,7 +128,7 @@ async fn new_public_account( async fn new_private_account( ctx: &mut crate::bench_context::BenchContext, - result: &mut ScenarioResult, + output: &mut ScenarioOutput, label: &str, ) -> Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -142,7 +142,7 @@ async fn new_private_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/e2e_bench/src/scenarios/token.rs b/tools/e2e_bench/src/scenarios/token.rs index c81bd8c1..24c38fc3 100644 --- a/tools/e2e_bench/src/scenarios/token.rs +++ b/tools/e2e_bench/src/scenarios/token.rs @@ -10,14 +10,14 @@ use wallet::cli::{ programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioResult, finalize_step}; +use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { - let mut result = ScenarioResult::new("token_onboarding"); +pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { + let mut output = ScenarioOutput::new("token_onboarding"); - let definition_id = new_public_account(ctx, &mut result, "create_pub_definition").await?; - let supply_id = new_public_account(ctx, &mut result, "create_pub_supply").await?; - let recipient_id = new_public_account(ctx, &mut result, "create_pub_recipient").await?; + let definition_id = new_public_account(ctx, &mut output, "create_pub_definition").await?; + let supply_id = new_public_account(ctx, &mut output, "create_pub_supply").await?; + let recipient_id = new_public_account(ctx, &mut output, "create_pub_recipient").await?; { let pre_block = crate::harness::begin_step(ctx).await?; @@ -33,7 +33,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result Result Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -96,7 +96,7 @@ async fn new_public_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -105,7 +105,7 @@ async fn new_public_account( async fn new_private_account( ctx: &mut crate::bench_context::BenchContext, - result: &mut ScenarioResult, + output: &mut ScenarioOutput, label: &str, ) -> Result { let pre_block = crate::harness::begin_step(ctx).await?; @@ -119,7 +119,7 @@ async fn new_private_account( ) .await?; let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - result.push(step); + output.push(step); match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), From 563a9ce0f7a3f964611a6043f24b93dbc0eef00d Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 20 May 2026 10:08:24 +0200 Subject: [PATCH 06/11] refactor: extract test_fixtures crate from integration_tests --- Cargo.lock | 31 +- Cargo.toml | 2 + integration_tests/Cargo.toml | 2 + integration_tests/src/lib.rs | 445 +--------------- test_fixtures/Cargo.toml | 34 ++ .../src/config.rs | 0 .../src/indexer_client.rs | 0 test_fixtures/src/lib.rs | 500 ++++++++++++++++++ .../src/setup.rs | 0 tools/e2e_bench/Cargo.toml | 2 +- tools/e2e_bench/src/bench_context.rs | 2 +- tools/e2e_bench/src/scenarios/amm.rs | 2 +- tools/e2e_bench/src/scenarios/fanout.rs | 2 +- tools/e2e_bench/src/scenarios/parallel.rs | 2 +- tools/e2e_bench/src/scenarios/private.rs | 2 +- tools/e2e_bench/src/scenarios/token.rs | 2 +- 16 files changed, 580 insertions(+), 448 deletions(-) create mode 100644 test_fixtures/Cargo.toml rename {integration_tests => test_fixtures}/src/config.rs (100%) rename {integration_tests => test_fixtures}/src/indexer_client.rs (100%) create mode 100644 test_fixtures/src/lib.rs rename {integration_tests => test_fixtures}/src/setup.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f238fc2f..ba2b0c5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2389,7 +2389,6 @@ dependencies = [ "common", "indexer_service", "indexer_service_rpc", - "integration_tests", "jsonrpsee", "nssa", "sequencer_service", @@ -2397,6 +2396,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "test_fixtures", "tokio", "wallet", ] @@ -4016,6 +4016,7 @@ dependencies = [ "sequencer_service_rpc", "serde_json", "tempfile", + "test_fixtures", "testcontainers", "token_core", "tokio", @@ -9221,6 +9222,34 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "test_fixtures" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytesize", + "common", + "env_logger", + "futures", + "indexer_service", + "jsonrpsee", + "key_protocol", + "log", + "nssa", + "nssa_core", + "sequencer_core", + "sequencer_service", + "sequencer_service_rpc", + "serde", + "serde_json", + "tempfile", + "testcontainers", + "tokio", + "url", + "vault_core", + "wallet", +] + [[package]] name = "test_program_methods" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d75e26c0..169e0cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "examples/program_deployment/methods/guest", "testnet_initial_state", "indexer/ffi", + "test_fixtures", "tools/cycle_bench", "tools/crypto_primitives_bench", "tools/e2e_bench", @@ -77,6 +78,7 @@ vault_core = { path = "programs/vault/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } integration_tests = { path = "integration_tests" } +test_fixtures = { path = "test_fixtures" } tokio = { version = "1.50", features = [ "net", diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 536f30bc..04cd8f8c 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -8,6 +8,8 @@ license = { workspace = true } workspace = true [dependencies] +test_fixtures.workspace = true + nssa_core = { workspace = true, features = ["host"] } nssa.workspace = true authenticated_transfer_core.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 3662e006..d3fa7c64 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -1,441 +1,6 @@ -//! This library contains common code for integration tests. +//! Integration test helpers, re-exported from `test_fixtures` for backwards +//! compatibility. The actual fixtures live in the `test_fixtures` crate so that +//! non-test consumers (e.g. `integration_bench`) can depend on them without +//! pulling in the test files. -use std::{net::SocketAddr, sync::LazyLock}; - -use anyhow::{Context as _, Result}; -use common::{HashType, transaction::NSSATransaction}; -use futures::FutureExt as _; -use indexer_service::IndexerHandle; -use log::{debug, error}; -use nssa::{AccountId, PrivacyPreservingTransaction}; -use nssa_core::Commitment; -use sequencer_core::config::GenesisAction; -use sequencer_service::SequencerHandle; -use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; -use tempfile::TempDir; -use testcontainers::compose::DockerCompose; -use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention}; - -use crate::{ - indexer_client::IndexerClient, - setup::{ - setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, - setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet, - }, -}; - -pub mod config; -pub mod indexer_client; -pub mod setup; - -// TODO: Remove this and control time from tests -pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; -pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; -pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; -pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; - -const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; -const BEDROCK_SERVICE_PORT: u16 = 18080; - -static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); - -struct IndexerComponents { - indexer_handle: IndexerHandle, - indexer_client: IndexerClient, - _temp_dir: TempDir, -} - -impl Drop for IndexerComponents { - fn drop(&mut self) { - let Self { - indexer_handle, - indexer_client: _, - _temp_dir: _, - } = self; - - if !indexer_handle.is_healthy() { - error!("Indexer handle has unexpectedly stopped before IndexerComponents drop"); - } - } -} - -/// Test context which sets up a sequencer and a wallet for integration tests. -/// -/// It's memory and logically safe to create multiple instances of this struct in parallel tests, -/// as each instance uses its own temporary directories for sequencer and wallet data. -// NOTE: Order of fields is important for proper drop order. -pub struct TestContext { - sequencer_client: SequencerClient, - wallet: WalletCore, - wallet_password: String, - /// Optional to move out value in Drop. - sequencer_handle: Option, - indexer_components: Option, - bedrock_compose: DockerCompose, - bedrock_addr: SocketAddr, - _temp_sequencer_dir: TempDir, - _temp_wallet_dir: TempDir, -} - -impl TestContext { - /// Create new test context. - pub async fn new() -> Result { - Self::builder().build().await - } - - /// Get a builder for the test context to customize its configuration. - #[must_use] - pub const fn builder() -> TestContextBuilder { - TestContextBuilder::new() - } - - /// Get reference to the wallet. - #[must_use] - pub const fn wallet(&self) -> &WalletCore { - &self.wallet - } - - #[must_use] - pub fn wallet_password(&self) -> &str { - &self.wallet_password - } - - /// Get mutable reference to the wallet. - pub const fn wallet_mut(&mut self) -> &mut WalletCore { - &mut self.wallet - } - - /// Get reference to the sequencer client. - #[must_use] - pub const fn sequencer_client(&self) -> &SequencerClient { - &self.sequencer_client - } - - /// Get the Bedrock Node address. - #[must_use] - pub const fn bedrock_addr(&self) -> SocketAddr { - self.bedrock_addr - } - - /// Get reference to the indexer. - /// - /// # Panics - /// - /// Panics if the indexer is not enabled in the test context. See - /// [`TestContextBuilder::disable_indexer()`]. - #[must_use] - pub fn indexer(&self) -> &IndexerHandle { - self.indexer_components - .as_ref() - .map(|components| &components.indexer_handle) - .expect("Called `TestContext::indexer()` on context with disabled indexer") - } - - /// Get reference to the indexer client. - /// - /// # Panics - /// - /// Panics if the indexer is not enabled in the test context. See - /// [`TestContextBuilder::disable_indexer()`]. - #[must_use] - pub fn indexer_client(&self) -> &IndexerClient { - self.indexer_components - .as_ref() - .map(|components| &components.indexer_client) - .expect("Called `TestContext::indexer_client()` on context with disabled indexer") - } - - /// Get existing public account IDs in the wallet. - #[must_use] - pub fn existing_public_accounts(&self) -> Vec { - self.wallet - .storage() - .key_chain() - .public_account_ids() - .map(|(account_id, _idx)| account_id) - .collect() - } - - /// Get existing private account IDs in the wallet. - #[must_use] - pub fn existing_private_accounts(&self) -> Vec { - self.wallet - .storage() - .key_chain() - .private_account_ids() - .map(|(account_id, _idx)| account_id) - .collect() - } -} - -impl Drop for TestContext { - fn drop(&mut self) { - let Self { - sequencer_handle, - bedrock_compose, - bedrock_addr: _, - indexer_components: _, - sequencer_client: _, - wallet: _, - wallet_password: _, - _temp_sequencer_dir: _, - _temp_wallet_dir: _, - } = self; - - let sequencer_handle = sequencer_handle - .take() - .expect("Sequencer handle should be present in TestContext drop"); - if !sequencer_handle.is_healthy() { - let Err(err) = sequencer_handle - .failed() - .now_or_never() - .expect("Sequencer handle should not be running"); - error!( - "Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}" - ); - } - - let container = bedrock_compose - .service(BEDROCK_SERVICE_WITH_OPEN_PORT) - .unwrap_or_else(|| { - panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`") - }); - let output = std::process::Command::new("docker") - .args(["inspect", "-f", "{{.State.Running}}", container.id()]) - .output() - .expect("Failed to execute docker inspect command to check if Bedrock container is still running"); - let stdout = String::from_utf8(output.stdout) - .expect("Failed to parse docker inspect output as String"); - if stdout.trim() != "true" { - error!( - "Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}", - container.id() - ); - } - } -} - -pub struct TestContextBuilder { - genesis_transactions: Option>, - sequencer_partial_config: Option, - enable_indexer: bool, -} - -impl TestContextBuilder { - const fn new() -> Self { - Self { - genesis_transactions: None, - sequencer_partial_config: None, - enable_indexer: true, - } - } - - #[must_use] - pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { - self.genesis_transactions = Some(genesis_transactions); - self - } - - #[must_use] - pub const fn with_sequencer_partial_config( - mut self, - sequencer_partial_config: config::SequencerPartialConfig, - ) -> Self { - self.sequencer_partial_config = Some(sequencer_partial_config); - self - } - - /// Exclude Indexer from test context. - /// Indexer is enabled by default. - /// - /// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if - /// called when indexer is disabled. - #[must_use] - pub const fn disable_indexer(mut self) -> Self { - self.enable_indexer = false; - self - } - - pub async fn build(self) -> Result { - let Self { - genesis_transactions, - sequencer_partial_config, - enable_indexer, - } = self; - - // Ensure logger is initialized only once - *LOGGER; - - debug!("Test context setup"); - - let (bedrock_compose, bedrock_addr) = setup_bedrock_node() - .await - .context("Failed to setup Bedrock node")?; - - let indexer_components = if enable_indexer { - let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) - .await - .context("Failed to setup Indexer")?; - let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) - .context("Failed to convert indexer addr to URL")?; - let indexer_client = IndexerClient::new(&indexer_url) - .await - .context("Failed to create indexer client")?; - Some(IndexerComponents { - indexer_handle, - indexer_client, - _temp_dir: temp_indexer_dir, - }) - } else { - None - }; - - let initial_public_accounts = config::default_public_accounts_for_wallet(); - let initial_private_accounts = config::default_private_accounts_for_wallet(); - let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( - sequencer_partial_config.unwrap_or_default(), - bedrock_addr, - genesis_transactions.unwrap_or_else(|| { - config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts) - }), - ) - .await - .context("Failed to setup Sequencer")?; - - let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet( - sequencer_handle.addr(), - &initial_public_accounts, - &initial_private_accounts, - ) - .context("Failed to setup wallet")?; - - setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts) - .await - .context("Failed to initialize public accounts in wallet")?; - - setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts) - .await - .context("Failed to initialize private accounts in wallet")?; - - let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) - .context("Failed to convert sequencer addr to URL")?; - let sequencer_client = SequencerClientBuilder::default() - .build(sequencer_url) - .context("Failed to create sequencer client")?; - - Ok(TestContext { - sequencer_client, - wallet, - wallet_password, - bedrock_compose, - bedrock_addr, - sequencer_handle: Some(sequencer_handle), - indexer_components, - _temp_sequencer_dir: temp_sequencer_dir, - _temp_wallet_dir: temp_wallet_dir, - }) - } - - pub fn build_blocking(self) -> Result { - let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; - - let ctx = runtime.block_on(self.build())?; - - Ok(BlockingTestContext { - ctx: Some(ctx), - runtime, - }) - } -} -/// A test context to be used in normal #[test] tests. -pub struct BlockingTestContext { - ctx: Option, - runtime: tokio::runtime::Runtime, -} - -impl BlockingTestContext { - pub fn new() -> Result { - TestContext::builder().build_blocking() - } - - pub const fn ctx(&self) -> &TestContext { - self.ctx.as_ref().expect("TestContext is set") - } - - pub const fn runtime(&self) -> &tokio::runtime::Runtime { - &self.runtime - } - - pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output - where - F: std::future::Future + 'ctx, - { - let future = f(self.ctx()); - self.runtime.block_on(future) - } - - pub fn block_on_mut<'ctx, F>( - &'ctx mut self, - f: impl FnOnce(&'ctx mut TestContext) -> F, - ) -> F::Output - where - F: std::future::Future + 'ctx, - { - let ctx_mut = self.ctx.as_mut().expect("TestContext is set"); - let future = f(ctx_mut); - self.runtime.block_on(future) - } -} - -impl Drop for BlockingTestContext { - fn drop(&mut self) { - let Self { ctx, runtime } = self; - - // Ensure async cleanup of TestContext by blocking on its drop in the runtime. - runtime.block_on(async { - if let Some(ctx) = ctx.take() { - drop(ctx); - } - }); - } -} - -#[must_use] -pub const fn public_mention(account_id: AccountId) -> CliAccountMention { - CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)) -} - -#[must_use] -pub const fn private_mention(account_id: AccountId) -> CliAccountMention { - CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id)) -} - -#[expect( - clippy::wildcard_enum_match_arm, - reason = "We want the code to panic if the transaction type is not PrivacyPreserving" -)] -pub async fn fetch_privacy_preserving_tx( - seq_client: &SequencerClient, - tx_hash: HashType, -) -> PrivacyPreservingTransaction { - let tx = seq_client.get_transaction(tx_hash).await.unwrap().unwrap(); - - match tx { - NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => { - privacy_preserving_transaction - } - _ => panic!("Invalid tx type"), - } -} - -pub async fn verify_commitment_is_in_state( - commitment: Commitment, - seq_client: &SequencerClient, -) -> bool { - seq_client - .get_proof_for_commitment(commitment) - .await - .ok() - .flatten() - .is_some() -} +pub use test_fixtures::*; diff --git a/test_fixtures/Cargo.toml b/test_fixtures/Cargo.toml new file mode 100644 index 00000000..1bfd2284 --- /dev/null +++ b/test_fixtures/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "test_fixtures" +version = "0.1.0" +edition = "2024" +license = { workspace = true } +publish = false + +[lints] +workspace = true + +[dependencies] +common.workspace = true +indexer_service.workspace = true +key_protocol.workspace = true +nssa.workspace = true +nssa_core = { workspace = true, features = ["host"] } +sequencer_core = { workspace = true, features = ["default", "testnet"] } +sequencer_service.workspace = true +sequencer_service_rpc = { workspace = true, features = ["client"] } +vault_core.workspace = true +wallet.workspace = true + +anyhow.workspace = true +bytesize.workspace = true +env_logger.workspace = true +futures.workspace = true +jsonrpsee = { workspace = true, features = ["ws-client"] } +log.workspace = true +serde.workspace = true +serde_json.workspace = true +tempfile.workspace = true +testcontainers = { version = "0.27.3", features = ["docker-compose"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +url.workspace = true diff --git a/integration_tests/src/config.rs b/test_fixtures/src/config.rs similarity index 100% rename from integration_tests/src/config.rs rename to test_fixtures/src/config.rs diff --git a/integration_tests/src/indexer_client.rs b/test_fixtures/src/indexer_client.rs similarity index 100% rename from integration_tests/src/indexer_client.rs rename to test_fixtures/src/indexer_client.rs diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs new file mode 100644 index 00000000..da2b7be2 --- /dev/null +++ b/test_fixtures/src/lib.rs @@ -0,0 +1,500 @@ +//! Shared test/bench fixtures: spins up bedrock + sequencer + indexer + wallet +//! end-to-end against docker-compose, exposes a `TestContext` callers can drive. +//! +//! Originally lived under `integration_tests`; split out so non-test consumers +//! (e.g. `integration_bench`) can depend on the fixtures without pulling in the +//! `integration_tests` test files. + +use std::{net::SocketAddr, path::Path, sync::LazyLock}; + +use anyhow::{Context as _, Result}; +use common::{HashType, transaction::NSSATransaction}; +use futures::FutureExt as _; +use indexer_service::IndexerHandle; +use log::{debug, error}; +use nssa::{AccountId, PrivacyPreservingTransaction}; +use nssa_core::Commitment; +use sequencer_core::config::GenesisAction; +use sequencer_service::SequencerHandle; +use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; +use serde::Serialize; +use tempfile::TempDir; +use testcontainers::compose::DockerCompose; +use wallet::{WalletCore, account::AccountIdWithPrivacy, cli::CliAccountMention}; + +use crate::{ + indexer_client::IndexerClient, + setup::{ + setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, + setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet, + }, +}; + +pub mod config; +pub mod indexer_client; +pub mod setup; + +// TODO: Remove this and control time from tests +pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; +pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; +pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; +pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; + +pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; +pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080; + +static LOGGER: LazyLock<()> = LazyLock::new(env_logger::init); + +struct IndexerComponents { + indexer_handle: IndexerHandle, + indexer_client: IndexerClient, + temp_dir: TempDir, +} + +impl Drop for IndexerComponents { + fn drop(&mut self) { + let Self { + indexer_handle, + indexer_client: _, + temp_dir: _, + } = self; + + if !indexer_handle.is_healthy() { + error!("Indexer handle has unexpectedly stopped before IndexerComponents drop"); + } + } +} + +/// Recursively-sized bytes on disk for sequencer / indexer / wallet tempdirs. +#[derive(Debug, Clone, Copy, Default, Serialize)] +pub struct DiskSizes { + pub sequencer_bytes: u64, + pub indexer_bytes: u64, + pub wallet_bytes: u64, +} + +/// Test context which sets up a sequencer and a wallet for integration tests. +/// +/// It's memory and logically safe to create multiple instances of this struct in parallel tests, +/// as each instance uses its own temporary directories for sequencer and wallet data. +// NOTE: Order of fields is important for proper drop order. +pub struct TestContext { + sequencer_client: SequencerClient, + wallet: WalletCore, + wallet_password: String, + /// Optional to move out value in Drop. + sequencer_handle: Option, + indexer_components: Option, + bedrock_compose: DockerCompose, + bedrock_addr: SocketAddr, + temp_sequencer_dir: TempDir, + temp_wallet_dir: TempDir, +} + +impl TestContext { + /// Create new test context. + pub async fn new() -> Result { + Self::builder().build().await + } + + /// Get a builder for the test context to customize its configuration. + #[must_use] + pub const fn builder() -> TestContextBuilder { + TestContextBuilder::new() + } + + /// Get reference to the wallet. + #[must_use] + pub const fn wallet(&self) -> &WalletCore { + &self.wallet + } + + #[must_use] + pub fn wallet_password(&self) -> &str { + &self.wallet_password + } + + /// Get mutable reference to the wallet. + pub const fn wallet_mut(&mut self) -> &mut WalletCore { + &mut self.wallet + } + + /// Get reference to the sequencer client. + #[must_use] + pub const fn sequencer_client(&self) -> &SequencerClient { + &self.sequencer_client + } + + /// Get the Bedrock Node address. + #[must_use] + pub const fn bedrock_addr(&self) -> SocketAddr { + self.bedrock_addr + } + + /// Get reference to the indexer. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer(&self) -> &IndexerHandle { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_handle) + .expect("Called `TestContext::indexer()` on context with disabled indexer") + } + + /// Get the indexer's bound socket address. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. + #[must_use] + pub fn indexer_addr(&self) -> SocketAddr { + self.indexer().addr() + } + + /// Get reference to the indexer client. + /// + /// # Panics + /// + /// Panics if the indexer is not enabled in the test context. See + /// [`TestContextBuilder::disable_indexer()`]. + #[must_use] + pub fn indexer_client(&self) -> &IndexerClient { + self.indexer_components + .as_ref() + .map(|components| &components.indexer_client) + .expect("Called `TestContext::indexer_client()` on context with disabled indexer") + } + + /// Recursively-sized bytes on disk for sequencer + indexer + wallet tempdirs. + /// Indexer bytes are zero if the indexer is disabled. + #[must_use] + pub fn disk_sizes(&self) -> DiskSizes { + DiskSizes { + sequencer_bytes: dir_size_bytes(self.temp_sequencer_dir.path()), + indexer_bytes: self + .indexer_components + .as_ref() + .map_or(0, |c| dir_size_bytes(c.temp_dir.path())), + wallet_bytes: dir_size_bytes(self.temp_wallet_dir.path()), + } + } + + /// Get existing public account IDs in the wallet. + #[must_use] + pub fn existing_public_accounts(&self) -> Vec { + self.wallet + .storage() + .key_chain() + .public_account_ids() + .map(|(account_id, _idx)| account_id) + .collect() + } + + /// Get existing private account IDs in the wallet. + #[must_use] + pub fn existing_private_accounts(&self) -> Vec { + self.wallet + .storage() + .key_chain() + .private_account_ids() + .map(|(account_id, _idx)| account_id) + .collect() + } +} + +impl Drop for TestContext { + fn drop(&mut self) { + let Self { + sequencer_handle, + bedrock_compose, + bedrock_addr: _, + indexer_components: _, + sequencer_client: _, + wallet: _, + wallet_password: _, + temp_sequencer_dir: _, + temp_wallet_dir: _, + } = self; + + let sequencer_handle = sequencer_handle + .take() + .expect("Sequencer handle should be present in TestContext drop"); + if !sequencer_handle.is_healthy() { + let Err(err) = sequencer_handle + .failed() + .now_or_never() + .expect("Sequencer handle should not be running"); + error!( + "Sequencer handle has unexpectedly stopped before TestContext drop with error: {err:#}" + ); + } + + let container = bedrock_compose + .service(BEDROCK_SERVICE_WITH_OPEN_PORT) + .unwrap_or_else(|| { + panic!("Failed to get Bedrock service container `{BEDROCK_SERVICE_WITH_OPEN_PORT}`") + }); + let output = std::process::Command::new("docker") + .args(["inspect", "-f", "{{.State.Running}}", container.id()]) + .output() + .expect("Failed to execute docker inspect command to check if Bedrock container is still running"); + let stdout = String::from_utf8(output.stdout) + .expect("Failed to parse docker inspect output as String"); + if stdout.trim() != "true" { + error!( + "Bedrock container `{}` is not running during TestContext drop, docker inspect output: {stdout}", + container.id() + ); + } + } +} + +pub struct TestContextBuilder { + genesis_transactions: Option>, + sequencer_partial_config: Option, + enable_indexer: bool, +} + +impl TestContextBuilder { + const fn new() -> Self { + Self { + genesis_transactions: None, + sequencer_partial_config: None, + enable_indexer: true, + } + } + + #[must_use] + pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { + self.genesis_transactions = Some(genesis_transactions); + self + } + + #[must_use] + pub const fn with_sequencer_partial_config( + mut self, + sequencer_partial_config: config::SequencerPartialConfig, + ) -> Self { + self.sequencer_partial_config = Some(sequencer_partial_config); + self + } + + /// Exclude Indexer from test context. + /// Indexer is enabled by default. + /// + /// Methods like [`TestContext::indexer()`] and [`TestContext::indexer_client()`] will panic if + /// called when indexer is disabled. + #[must_use] + pub const fn disable_indexer(mut self) -> Self { + self.enable_indexer = false; + self + } + + pub async fn build(self) -> Result { + let Self { + genesis_transactions, + sequencer_partial_config, + enable_indexer, + } = self; + + // Ensure logger is initialized only once + *LOGGER; + + debug!("Test context setup"); + + let (bedrock_compose, bedrock_addr) = setup_bedrock_node() + .await + .context("Failed to setup Bedrock node")?; + + let indexer_components = if enable_indexer { + let (indexer_handle, temp_indexer_dir) = setup_indexer(bedrock_addr) + .await + .context("Failed to setup Indexer")?; + let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_handle.addr()) + .context("Failed to convert indexer addr to URL")?; + let indexer_client = IndexerClient::new(&indexer_url) + .await + .context("Failed to create indexer client")?; + Some(IndexerComponents { + indexer_handle, + indexer_client, + temp_dir: temp_indexer_dir, + }) + } else { + None + }; + + let initial_public_accounts = config::default_public_accounts_for_wallet(); + let initial_private_accounts = config::default_private_accounts_for_wallet(); + let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( + sequencer_partial_config.unwrap_or_default(), + bedrock_addr, + genesis_transactions.unwrap_or_else(|| { + config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts) + }), + ) + .await + .context("Failed to setup Sequencer")?; + + let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet( + sequencer_handle.addr(), + &initial_public_accounts, + &initial_private_accounts, + ) + .context("Failed to setup wallet")?; + + setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts) + .await + .context("Failed to initialize public accounts in wallet")?; + + setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts) + .await + .context("Failed to initialize private accounts in wallet")?; + + let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr()) + .context("Failed to convert sequencer addr to URL")?; + let sequencer_client = SequencerClientBuilder::default() + .build(sequencer_url) + .context("Failed to create sequencer client")?; + + Ok(TestContext { + sequencer_client, + wallet, + wallet_password, + bedrock_compose, + bedrock_addr, + sequencer_handle: Some(sequencer_handle), + indexer_components, + temp_sequencer_dir, + temp_wallet_dir, + }) + } + + pub fn build_blocking(self) -> Result { + let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; + + let ctx = runtime.block_on(self.build())?; + + Ok(BlockingTestContext { + ctx: Some(ctx), + runtime, + }) + } +} +/// A test context to be used in normal #[test] tests. +pub struct BlockingTestContext { + ctx: Option, + runtime: tokio::runtime::Runtime, +} + +impl BlockingTestContext { + pub fn new() -> Result { + TestContext::builder().build_blocking() + } + + pub const fn ctx(&self) -> &TestContext { + self.ctx.as_ref().expect("TestContext is set") + } + + pub const fn runtime(&self) -> &tokio::runtime::Runtime { + &self.runtime + } + + pub fn block_on<'ctx, F>(&'ctx self, f: impl FnOnce(&'ctx TestContext) -> F) -> F::Output + where + F: std::future::Future + 'ctx, + { + let future = f(self.ctx()); + self.runtime.block_on(future) + } + + pub fn block_on_mut<'ctx, F>( + &'ctx mut self, + f: impl FnOnce(&'ctx mut TestContext) -> F, + ) -> F::Output + where + F: std::future::Future + 'ctx, + { + let ctx_mut = self.ctx.as_mut().expect("TestContext is set"); + let future = f(ctx_mut); + self.runtime.block_on(future) + } +} + +impl Drop for BlockingTestContext { + fn drop(&mut self) { + let Self { ctx, runtime } = self; + + // Ensure async cleanup of TestContext by blocking on its drop in the runtime. + runtime.block_on(async { + if let Some(ctx) = ctx.take() { + drop(ctx); + } + }); + } +} + +#[must_use] +pub const fn public_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)) +} + +#[must_use] +pub const fn private_mention(account_id: AccountId) -> CliAccountMention { + CliAccountMention::Id(AccountIdWithPrivacy::Private(account_id)) +} + +#[expect( + clippy::wildcard_enum_match_arm, + reason = "We want the code to panic if the transaction type is not PrivacyPreserving" +)] +pub async fn fetch_privacy_preserving_tx( + seq_client: &SequencerClient, + tx_hash: HashType, +) -> PrivacyPreservingTransaction { + let tx = seq_client.get_transaction(tx_hash).await.unwrap().unwrap(); + + match tx { + NSSATransaction::PrivacyPreserving(privacy_preserving_transaction) => { + privacy_preserving_transaction + } + _ => panic!("Invalid tx type"), + } +} + +pub async fn verify_commitment_is_in_state( + commitment: Commitment, + seq_client: &SequencerClient, +) -> bool { + seq_client + .get_proof_for_commitment(commitment) + .await + .ok() + .flatten() + .is_some() +} + +fn dir_size_bytes(path: &Path) -> u64 { + let mut total = 0_u64; + let Ok(entries) = std::fs::read_dir(path) else { + return 0; + }; + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if metadata.is_file() { + total = total.saturating_add(metadata.len()); + } else if metadata.is_dir() { + total = total.saturating_add(dir_size_bytes(&entry.path())); + } else { + // Sockets, FIFOs, block/char devices: ignore. Symlinks are + // already followed by `is_file()` / `is_dir()`. + } + } + total +} diff --git a/integration_tests/src/setup.rs b/test_fixtures/src/setup.rs similarity index 100% rename from integration_tests/src/setup.rs rename to test_fixtures/src/setup.rs diff --git a/tools/e2e_bench/Cargo.toml b/tools/e2e_bench/Cargo.toml index ab6a6eb0..97d34f53 100644 --- a/tools/e2e_bench/Cargo.toml +++ b/tools/e2e_bench/Cargo.toml @@ -12,10 +12,10 @@ workspace = true common.workspace = true indexer_service.workspace = true indexer_service_rpc = { workspace = true, features = ["client"] } -integration_tests.workspace = true nssa.workspace = true sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } +test_fixtures.workspace = true wallet.workspace = true anyhow.workspace = true diff --git a/tools/e2e_bench/src/bench_context.rs b/tools/e2e_bench/src/bench_context.rs index 41f0d59d..e3de508e 100644 --- a/tools/e2e_bench/src/bench_context.rs +++ b/tools/e2e_bench/src/bench_context.rs @@ -15,7 +15,7 @@ use std::{env, net::SocketAddr, path::Path}; use anyhow::{Context as _, Result}; use indexer_service::IndexerHandle; -use integration_tests::config::{ +use test_fixtures::config::{ SequencerPartialConfig, UrlProtocol, addr_to_url, default_private_accounts_for_wallet, default_public_accounts_for_wallet, genesis_from_accounts, indexer_config, sequencer_config, wallet_config, diff --git a/tools/e2e_bench/src/scenarios/amm.rs b/tools/e2e_bench/src/scenarios/amm.rs index 6756321d..f295a4aa 100644 --- a/tools/e2e_bench/src/scenarios/amm.rs +++ b/tools/e2e_bench/src/scenarios/amm.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::public_mention; +use test_fixtures::public_mention; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/fanout.rs b/tools/e2e_bench/src/scenarios/fanout.rs index 59e9a64b..6f85a974 100644 --- a/tools/e2e_bench/src/scenarios/fanout.rs +++ b/tools/e2e_bench/src/scenarios/fanout.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::public_mention; +use test_fixtures::public_mention; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/e2e_bench/src/scenarios/parallel.rs index 86368a0d..29c3a72f 100644 --- a/tools/e2e_bench/src/scenarios/parallel.rs +++ b/tools/e2e_bench/src/scenarios/parallel.rs @@ -7,7 +7,7 @@ use std::time::Instant; use anyhow::{Result, bail}; use common::transaction::NSSATransaction; -use integration_tests::public_mention; +use test_fixtures::public_mention; use sequencer_service_rpc::RpcClient as _; use wallet::cli::{ Command, SubcommandReturnValue, diff --git a/tools/e2e_bench/src/scenarios/private.rs b/tools/e2e_bench/src/scenarios/private.rs index c6ef9888..f0b4745b 100644 --- a/tools/e2e_bench/src/scenarios/private.rs +++ b/tools/e2e_bench/src/scenarios/private.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::{private_mention, public_mention}; +use test_fixtures::{private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, diff --git a/tools/e2e_bench/src/scenarios/token.rs b/tools/e2e_bench/src/scenarios/token.rs index 24c38fc3..4e63da32 100644 --- a/tools/e2e_bench/src/scenarios/token.rs +++ b/tools/e2e_bench/src/scenarios/token.rs @@ -3,7 +3,7 @@ use std::time::Instant; use anyhow::{Result, bail}; -use integration_tests::{private_mention, public_mention}; +use test_fixtures::{private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, From 0119b38c1be882c2baeb50c655c934dd0811e795 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 20 May 2026 11:04:06 +0200 Subject: [PATCH 07/11] refactor(integration_bench)!: pivot to docker-compose via TestContext, share one node per run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: - crate renamed e2e_bench → integration_bench. Run via `cargo run -p integration_bench`. - env vars removed: LEZ_BEDROCK_BIN, LEZ_BEDROCK_CONFIG_DIR, LEZ_BEDROCK_PORT. Replaced by a docker prerequisite (docker-compose Bedrock via test_fixtures::TestContext). - output filenames: target/e2e_bench_{dev,prove}.json → target/integration_bench_{dev,prove}.json. - JSON schema: per-scenario `setup_s` field removed; replaced by run-level `shared_setup_s` (one TestContext is shared across all scenarios in a run). - internal: bedrock_handle.rs and bench_context.rs deleted; placeholder-string config (PLACEHOLDER_CHAIN_START_TIME) gone. --- Cargo.lock | 42 ++-- Cargo.toml | 2 +- docs/benchmarks/README.md | 2 +- .../{e2e_bench.md => integration_bench.md} | 31 ++- tools/e2e_bench/README.md | 33 --- tools/e2e_bench/src/bedrock_handle.rs | 152 ------------- tools/e2e_bench/src/bench_context.rs | 210 ------------------ .../Cargo.toml | 6 +- tools/integration_bench/README.md | 27 +++ .../src/harness.rs | 20 +- .../src/main.rs | 112 ++++------ .../src/scenarios/amm.rs | 8 +- .../src/scenarios/fanout.rs | 4 +- .../src/scenarios/mod.rs | 0 .../src/scenarios/parallel.rs | 11 +- .../src/scenarios/private.rs | 6 +- .../src/scenarios/token.rs | 6 +- 17 files changed, 135 insertions(+), 537 deletions(-) rename docs/benchmarks/{e2e_bench.md => integration_bench.md} (62%) delete mode 100644 tools/e2e_bench/README.md delete mode 100644 tools/e2e_bench/src/bedrock_handle.rs delete mode 100644 tools/e2e_bench/src/bench_context.rs rename tools/{e2e_bench => integration_bench}/Cargo.toml (82%) create mode 100644 tools/integration_bench/README.md rename tools/{e2e_bench => integration_bench}/src/harness.rs (95%) rename tools/{e2e_bench => integration_bench}/src/main.rs (60%) rename tools/{e2e_bench => integration_bench}/src/scenarios/amm.rs (96%) rename tools/{e2e_bench => integration_bench}/src/scenarios/fanout.rs (95%) rename tools/{e2e_bench => integration_bench}/src/scenarios/mod.rs (100%) rename tools/{e2e_bench => integration_bench}/src/scenarios/parallel.rs (96%) rename tools/{e2e_bench => integration_bench}/src/scenarios/private.rs (96%) rename tools/{e2e_bench => integration_bench}/src/scenarios/token.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index ba2b0c5b..7efb7761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2378,29 +2378,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "e2e_bench" -version = "0.1.0" -dependencies = [ - "anyhow", - "borsh", - "chrono", - "clap", - "common", - "indexer_service", - "indexer_service_rpc", - "jsonrpsee", - "nssa", - "sequencer_service", - "sequencer_service_rpc", - "serde", - "serde_json", - "tempfile", - "test_fixtures", - "tokio", - "wallet", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -3989,6 +3966,25 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "integration_bench" +version = "0.1.0" +dependencies = [ + "anyhow", + "borsh", + "clap", + "common", + "indexer_service_rpc", + "jsonrpsee", + "nssa", + "sequencer_service_rpc", + "serde", + "serde_json", + "test_fixtures", + "tokio", + "wallet", +] + [[package]] name = "integration_tests" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 169e0cb8..ce67c92a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ members = [ "test_fixtures", "tools/cycle_bench", "tools/crypto_primitives_bench", - "tools/e2e_bench", + "tools/integration_bench", ] [workspace.dependencies] diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md index db539966..d745f2f3 100644 --- a/docs/benchmarks/README.md +++ b/docs/benchmarks/README.md @@ -6,6 +6,6 @@ Bench tools live under `tools/` with READMEs for how to run each one. This direc |---|---| | cycle_bench | [cycle_bench.md](cycle_bench.md) | | crypto_primitives_bench | [crypto_primitives_bench.md](crypto_primitives_bench.md) | -| e2e_bench | [e2e_bench.md](e2e_bench.md) | +| integration_bench | [integration_bench.md](integration_bench.md) | All numbers are from a single M2 Pro dev box unless noted otherwise. diff --git a/docs/benchmarks/e2e_bench.md b/docs/benchmarks/integration_bench.md similarity index 62% rename from docs/benchmarks/e2e_bench.md rename to docs/benchmarks/integration_bench.md index 278dab9b..a1295dec 100644 --- a/docs/benchmarks/e2e_bench.md +++ b/docs/benchmarks/integration_bench.md @@ -1,6 +1,6 @@ -# e2e_bench +# integration_bench -End-to-end LEZ scenarios driven through the wallet against an in-process sequencer + indexer wired to an external Bedrock node. Times each step and records borsh sizes per block, split by tx variant. +End-to-end LEZ scenarios driven through the wallet against a docker-compose Bedrock node + in-process sequencer + indexer (via `test_fixtures::TestContext`). Times each step and records borsh sizes per block, split by tx variant. No numeric tables here yet. Absolute wall time and block sizes depend heavily on the bedrock config (block cadence and confirmation depth) and on dev-mode vs real proving; re-run the bench locally to get numbers for your own setup. Canonical numbers will be added once the bench runs against the standard configuration. @@ -31,30 +31,29 @@ Numbers are intentionally omitted in this document until the canonical run lands ## Methodology -Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. Empty clock-only ticks give the per-block fixed-cost baseline. Wall time is captured per step (submit + inclusion + wallet sync) and per scenario (setup + steps + closing bedrock finality wait). +Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. Empty clock-only ticks give the per-block fixed-cost baseline. Wall time is captured per step (submit + inclusion + wallet sync) and aggregated to the per-scenario `total_s`. The one-time stack-setup cost (`shared_setup_s` at the run level) and the closing bedrock finality wait (`bedrock_finality_s` per scenario) are reported separately, not folded into `total_s`. ## Reproduce +Prerequisite: a running local Docker daemon (the `bedrock/docker-compose.yml` is brought up by the bench). + ```sh -export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node -export LEZ_BEDROCK_CONFIG_DIR=/path/to/bedrock/configs +# Dev-mode sweep (fast) +RISC0_DEV_MODE=1 cargo run --release -p integration_bench -- --scenario all -# Dev-mode sweep (fast, ~16 min for all five scenarios) -RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all +# Real-proving for representative private flow +cargo run --release -p integration_bench -- --scenario private -# Real-proving for representative private flow (~6 min on M2 Pro CPU) -cargo run --release -p e2e_bench -- --scenario private - -# Real-proving for representative public flow (~3 min) -cargo run --release -p e2e_bench -- --scenario amm +# Real-proving for representative public flow +cargo run --release -p integration_bench -- --scenario amm ``` -JSON output: `target/e2e_bench_dev.json` / `target/e2e_bench_prove.json` (suffix toggled by `RISC0_DEV_MODE`). +JSON output: `target/integration_bench_dev.json` / `target/integration_bench_prove.json` (suffix toggled by `RISC0_DEV_MODE`). ## Caveats - Dev-mode `ppe_tx_bytes` and PPE-step latencies are not representative of production; use real-proving numbers for any fee-model input that touches the storage or prover-cost components. - Single-host run, no GPU acceleration. Real-proving on production prover hardware will move per-step latencies by orders of magnitude; byte counts will not change. -- Bedrock running locally; no real network latency between sequencer and Bedrock. -- Bedrock L1 finality (`bedrock_finality_s`) is set by the bedrock config in `LEZ_BEDROCK_CONFIG_DIR` (block cadence × confirmation depth). Different configs will shift `bedrock_finality_s` materially. -- Some scenarios share account state via the same wallet; this is intentional (mirrors `integration_tests::TestContext`) and not a realistic multi-wallet workload. +- Bedrock running locally via docker-compose; no real network latency between sequencer and Bedrock. +- Bedrock L1 finality (`bedrock_finality_s`) is set by the bedrock config in `bedrock/docker-compose.yml` (block cadence × confirmation depth). Different configs will shift `bedrock_finality_s` materially. +- All scenarios share a single TestContext for the run (one bedrock + sequencer + indexer + wallet for the whole run, chain state accumulating across scenarios), which matches how the node runs in production. diff --git a/tools/e2e_bench/README.md b/tools/e2e_bench/README.md deleted file mode 100644 index ddc76bb7..00000000 --- a/tools/e2e_bench/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# e2e_bench - -End-to-end LEZ scenarios driven through the wallet against an in-process sequencer + indexer wired to an external Bedrock node. Times each step (submit, inclusion, wallet sync) and records borsh sizes for every block produced, split into per-tx-variant counts. - -## Run - -Required env vars (no defaults): - -```sh -export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node -export LEZ_BEDROCK_CONFIG_DIR=/path/to/bedrock/configs -# optional: LEZ_BEDROCK_PORT (default 18080) -``` - -The config dir must contain `node-config.yaml` and a `deployment-settings.yaml` template with the literal string `PLACEHOLDER_CHAIN_START_TIME` (rewritten per launch). - -```sh -# All scenarios, dev-mode proving (fast) -RISC0_DEV_MODE=1 cargo run --release -p e2e_bench -- --scenario all - -# One scenario, real proving (slow) -cargo run --release -p e2e_bench -- --scenario amm -``` - -Scenarios: `token`, `amm`, `fanout`, `private`, `parallel`, `all`. - -## What you'll see - -Per scenario: a step table (`submit_s`, `inclusion_s`, `sync_s`, `total_s`) and a size summary covering every block captured during the scenario (block_bytes total/mean/min/max; per-tx-variant sizes for public, PPE, and program-deployment transactions). - -The fanout, parallel, and private scenarios are the most representative for L1-payload-size measurements since they put multiple txs per block. - -JSON output is written to `target/e2e_bench.json`. diff --git a/tools/e2e_bench/src/bedrock_handle.rs b/tools/e2e_bench/src/bedrock_handle.rs deleted file mode 100644 index 94a8514a..00000000 --- a/tools/e2e_bench/src/bedrock_handle.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Manages an external `logos-blockchain-node` process as a child of the bench. -//! Launches a fresh Bedrock instance per scenario so the indexer never has to -//! catch up a large finalization backlog. -//! -//! Required env vars (no defaults, path layouts differ per developer): -//! - `LEZ_BEDROCK_BIN` absolute path to the `logos-blockchain-node` binary. -//! - `LEZ_BEDROCK_CONFIG_DIR` directory containing `node-config.yaml` and -//! `deployment-settings.yaml` (template with `PLACEHOLDER_CHAIN_START_TIME`). -//! -//! Optional: -//! - `LEZ_BEDROCK_PORT` (default: 18080) - -#![allow( - clippy::let_underscore_must_use, - reason = "file is deleted in the docker-compose pivot; teardown ignores child kill/wait results by design" -)] - -use std::{ - env, - net::SocketAddr, - path::PathBuf, - process::{Child, Command, Stdio}, - time::{Duration, Instant}, -}; - -use anyhow::{Context as _, Result, bail}; - -pub struct BedrockHandle { - child: Option, - addr: SocketAddr, - workdir: PathBuf, -} - -impl BedrockHandle { - /// Launch a fresh Bedrock node. Cleans `state/` in the working dir, rewrites - /// `deployment-settings.yaml` with the current UTC `chain_start_time`, spawns - /// the binary, and polls the HTTP port until ready. - pub async fn launch_fresh() -> Result { - let bin = env::var("LEZ_BEDROCK_BIN").map_err(|err| { - anyhow::anyhow!( - "LEZ_BEDROCK_BIN is required ({err}). Set it to the absolute path of the \ - logos-blockchain-node binary (e.g. \ - `export LEZ_BEDROCK_BIN=/path/to/logos-blockchain/target/release/logos-blockchain-node`)." - ) - })?; - let config_dir = env::var("LEZ_BEDROCK_CONFIG_DIR").map_err(|err| { - anyhow::anyhow!( - "LEZ_BEDROCK_CONFIG_DIR is required ({err}). Set it to the directory containing \ - node-config.yaml and deployment-settings.yaml \ - (see tools/e2e_bench/README.md for the expected layout)." - ) - })?; - let port: u16 = env::var("LEZ_BEDROCK_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(18080); - - let bin_path = PathBuf::from(&bin); - if !bin_path.is_file() { - bail!( - "LEZ_BEDROCK_BIN does not point at a file: {bin}. Build it via \ - `cargo build -p logos-blockchain-node --release` in logos-blockchain." - ); - } - let config_dir = PathBuf::from(config_dir); - let node_config = config_dir.join("node-config.yaml"); - let dep_template = config_dir.join("deployment-settings.yaml"); - if !node_config.is_file() || !dep_template.is_file() { - bail!( - "LEZ_BEDROCK_CONFIG_DIR is missing node-config.yaml or \ - deployment-settings.yaml at {}", - config_dir.display() - ); - } - - let workdir = tempfile::tempdir() - .context("create bedrock workdir")? - .keep(); - let dep_runtime = workdir.join("deployment-settings.yaml"); - let raw = std::fs::read_to_string(&dep_template).context("read deployment template")?; - let timestamp = chrono_now_utc_string(); - let filled = raw.replace("PLACEHOLDER_CHAIN_START_TIME", ×tamp); - std::fs::write(&dep_runtime, filled).context("write deployment-settings runtime")?; - - let log_path = workdir.join("bedrock.log"); - let log_file = std::fs::File::create(&log_path).context("create bedrock log")?; - let log_err = log_file.try_clone().context("clone bedrock log")?; - - eprintln!( - "BedrockHandle: launching {} (workdir {})", - bin, - workdir.display() - ); - let child = Command::new(&bin_path) - .current_dir(&workdir) - .arg("--deployment") - .arg(&dep_runtime) - .arg(&node_config) - .env("POL_PROOF_DEV_MODE", "true") - .stdout(Stdio::from(log_file)) - .stderr(Stdio::from(log_err)) - .spawn() - .context("spawn logos-blockchain-node")?; - - let addr = SocketAddr::from(([127, 0, 0, 1], port)); - wait_for_http(addr, Duration::from_secs(60)) - .await - .context("bedrock HTTP did not come up in 60s")?; - - eprintln!("BedrockHandle: stdout/stderr at {}", log_path.display()); - Ok(Self { - child: Some(child), - addr, - workdir, - }) - } - - pub const fn addr(&self) -> SocketAddr { - self.addr - } -} - -impl Drop for BedrockHandle { - fn drop(&mut self) { - if let Some(mut child) = self.child.take() { - eprintln!("BedrockHandle: stopping bedrock pid {}", child.id()); - let _ = child.kill(); - let _ = child.wait(); - } - let _ = std::fs::remove_dir_all(&self.workdir); - } -} - -async fn wait_for_http(addr: SocketAddr, timeout: Duration) -> Result<()> { - let deadline = Instant::now() + timeout; - while Instant::now() < deadline { - if tokio::net::TcpStream::connect(addr).await.is_ok() { - // TCP accepts; give Bedrock a moment to finish chain bootstrap. - tokio::time::sleep(Duration::from_secs(2)).await; - return Ok(()); - } - tokio::time::sleep(Duration::from_millis(250)).await; - } - bail!("Bedrock at {addr} did not accept TCP within {timeout:?}"); -} - -fn chrono_now_utc_string() -> String { - // Format: YYYY-MM-DD HH:MM:SS.000000 +00:00:00 (matches the deployment-settings template). - chrono::Utc::now() - .format("%Y-%m-%d %H:%M:%S%.6f +00:00:00") - .to_string() -} diff --git a/tools/e2e_bench/src/bench_context.rs b/tools/e2e_bench/src/bench_context.rs deleted file mode 100644 index e3de508e..00000000 --- a/tools/e2e_bench/src/bench_context.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! `BenchContext`: wires sequencer + indexer + wallet in-process against an -//! externally-running Bedrock node. Mirrors the surface of -//! `integration_tests::TestContext` for the methods the scenarios need -//! (`wallet_mut()`, `sequencer_client()`), but skips the docker setup. -//! -//! The external Bedrock URL defaults to 127.0.0.1:18080 and can be overridden -//! with the `LEZ_BEDROCK_ADDR` env var. - -#![allow( - clippy::arbitrary_source_item_ordering, - reason = "file is deleted in the docker-compose pivot; ordering churn is wasted work" -)] - -use std::{env, net::SocketAddr, path::Path}; - -use anyhow::{Context as _, Result}; -use indexer_service::IndexerHandle; -use test_fixtures::config::{ - SequencerPartialConfig, UrlProtocol, addr_to_url, default_private_accounts_for_wallet, - default_public_accounts_for_wallet, genesis_from_accounts, indexer_config, sequencer_config, - wallet_config, -}; -use sequencer_service::SequencerHandle; -use sequencer_service_rpc::{SequencerClient, SequencerClientBuilder}; -use serde::Serialize; -use tempfile::TempDir; -use wallet::{WalletCore, config::WalletConfigOverrides}; - -const DEFAULT_BEDROCK_ADDR: &str = "127.0.0.1:18080"; - -#[expect( - clippy::partial_pub_fields, - reason = "Internal TempDirs are kept alive via private fields for RAII; \ - client and wallet are public for scenarios to drive." -)] -pub struct BenchContext { - pub sequencer_client: SequencerClient, - pub wallet: WalletCore, - #[expect( - dead_code, - reason = "Retained for parity with TestContext; may be needed later." - )] - pub wallet_password: String, - sequencer_handle: Option, - indexer_handle: IndexerHandle, - temp_indexer_dir: TempDir, - temp_sequencer_dir: TempDir, - temp_wallet_dir: TempDir, -} - -impl BenchContext { - pub async fn new() -> Result { - let bedrock_addr_str = - env::var("LEZ_BEDROCK_ADDR").unwrap_or_else(|_| DEFAULT_BEDROCK_ADDR.to_owned()); - let bedrock_addr: SocketAddr = bedrock_addr_str - .parse() - .with_context(|| format!("invalid LEZ_BEDROCK_ADDR `{bedrock_addr_str}`"))?; - - eprintln!("BenchContext: using external bedrock at {bedrock_addr}"); - - let initial_public_accounts = default_public_accounts_for_wallet(); - let initial_private_accounts = default_private_accounts_for_wallet(); - let genesis_transactions = - genesis_from_accounts(&initial_public_accounts, &initial_private_accounts); - let sequencer_partial = SequencerPartialConfig::default(); - - let temp_indexer_dir = tempfile::tempdir().context("indexer temp dir")?; - let indexer_cfg = indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) - .context("indexer config")?; - let indexer_handle = indexer_service::run_server(indexer_cfg, 0) - .await - .context("indexer run_server")?; - - let temp_sequencer_dir = tempfile::tempdir().context("sequencer temp dir")?; - let sequencer_cfg = sequencer_config( - sequencer_partial, - temp_sequencer_dir.path().to_owned(), - bedrock_addr, - genesis_transactions, - ) - .context("sequencer config")?; - let sequencer_handle = sequencer_service::run(sequencer_cfg, 0) - .await - .context("sequencer run")?; - - let temp_wallet_dir = tempfile::tempdir().context("wallet temp dir")?; - let mut wallet_cfg = wallet_config(sequencer_handle.addr()).context("wallet config")?; - // The default 30s poll interval is far too slow for a measurement run; - // shrink so the wallet sees new blocks within ~1s. - wallet_cfg.seq_poll_timeout = std::time::Duration::from_secs(1); - let wallet_cfg_str = - serde_json::to_string_pretty(&wallet_cfg).context("serialize wallet config")?; - let wallet_cfg_path = temp_wallet_dir.path().join("wallet_config.json"); - std::fs::write(&wallet_cfg_path, wallet_cfg_str).context("write wallet config")?; - let storage_path = temp_wallet_dir.path().join("storage.json"); - let password = "bench_pass".to_owned(); - let (mut wallet, _mnemonic) = WalletCore::new_init_storage( - wallet_cfg_path, - storage_path, - Some(WalletConfigOverrides::default()), - &password, - ) - .context("wallet init")?; - // Mirror integration_tests::setup_wallet: import the initial accounts - // produced above so the wallet can reference them by AccountId in scenarios. - for (private_key, _balance) in &initial_public_accounts { - wallet - .storage_mut() - .key_chain_mut() - .add_imported_public_account(private_key.clone()); - } - for private_account in &initial_private_accounts { - wallet - .storage_mut() - .key_chain_mut() - .add_imported_private_account( - private_account.key_chain.clone(), - None, - private_account.identifier, - nssa::Account::default(), - ); - } - wallet - .store_persistent_data() - .context("wallet store persistent")?; - - let sequencer_url = - addr_to_url(UrlProtocol::Http, sequencer_handle.addr()).context("sequencer url")?; - let sequencer_client = SequencerClientBuilder::default() - .build(sequencer_url) - .context("build sequencer client")?; - - Ok(Self { - sequencer_client, - wallet, - wallet_password: password, - sequencer_handle: Some(sequencer_handle), - indexer_handle, - temp_indexer_dir, - temp_sequencer_dir, - temp_wallet_dir, - }) - } - - pub const fn wallet_mut(&mut self) -> &mut WalletCore { - &mut self.wallet - } - - pub const fn sequencer_client(&self) -> &SequencerClient { - &self.sequencer_client - } - - pub const fn indexer_addr(&self) -> SocketAddr { - self.indexer_handle.addr() - } - - /// Recursively-sized bytes on disk for sequencer + indexer + wallet tempdirs. - pub fn disk_sizes(&self) -> DiskSizes { - DiskSizes { - sequencer_bytes: dir_size_bytes(self.temp_sequencer_dir.path()), - indexer_bytes: dir_size_bytes(self.temp_indexer_dir.path()), - wallet_bytes: dir_size_bytes(self.temp_wallet_dir.path()), - } - } -} - -#[derive(Debug, Clone, Copy, Default, Serialize)] -#[expect( - clippy::struct_field_names, - reason = "The `_bytes` suffix carries the unit and is preserved verbatim in JSON output." -)] -pub struct DiskSizes { - pub sequencer_bytes: u64, - pub indexer_bytes: u64, - pub wallet_bytes: u64, -} - -fn dir_size_bytes(path: &Path) -> u64 { - let mut total = 0_u64; - let Ok(entries) = std::fs::read_dir(path) else { - return 0; - }; - for entry in entries.flatten() { - let Ok(metadata) = entry.metadata() else { - continue; - }; - if metadata.is_file() { - total = total.saturating_add(metadata.len()); - } else if metadata.is_dir() { - total = total.saturating_add(dir_size_bytes(&entry.path())); - } else { - // Sockets, FIFOs, block/char devices: ignore. Symlinks are - // already followed by `is_file()` / `is_dir()`. - } - } - total -} - -impl Drop for BenchContext { - fn drop(&mut self) { - if let Some(handle) = self.sequencer_handle.take() - && !handle.is_healthy() - { - eprintln!("BenchContext drop: sequencer handle was unhealthy"); - } - if !self.indexer_handle.is_healthy() { - eprintln!("BenchContext drop: indexer handle was unhealthy"); - } - } -} diff --git a/tools/e2e_bench/Cargo.toml b/tools/integration_bench/Cargo.toml similarity index 82% rename from tools/e2e_bench/Cargo.toml rename to tools/integration_bench/Cargo.toml index 97d34f53..0829a7f0 100644 --- a/tools/e2e_bench/Cargo.toml +++ b/tools/integration_bench/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "e2e_bench" +name = "integration_bench" version = "0.1.0" edition = "2024" license = { workspace = true } @@ -10,20 +10,16 @@ workspace = true [dependencies] common.workspace = true -indexer_service.workspace = true indexer_service_rpc = { workspace = true, features = ["client"] } nssa.workspace = true -sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } test_fixtures.workspace = true wallet.workspace = true anyhow.workspace = true borsh.workspace = true -chrono.workspace = true clap.workspace = true jsonrpsee = { workspace = true, features = ["ws-client"] } serde.workspace = true serde_json.workspace = true -tempfile.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } diff --git a/tools/integration_bench/README.md b/tools/integration_bench/README.md new file mode 100644 index 00000000..f6e2ee04 --- /dev/null +++ b/tools/integration_bench/README.md @@ -0,0 +1,27 @@ +# integration_bench + +End-to-end LEZ scenarios driven through the wallet against a docker-compose Bedrock node + in-process sequencer + indexer (via `test_fixtures::TestContext`). Times each step (submit, inclusion, wallet sync) and records borsh sizes for every block produced, split into per-tx-variant counts. + +## Run + +Prerequisite: a running local Docker daemon. The Bedrock service comes up via the same `bedrock/docker-compose.yml` that integration tests use, so no host-side binary or env vars are required. + +```sh +# All scenarios, dev-mode proving (fast) +RISC0_DEV_MODE=1 cargo run --release -p integration_bench -- --scenario all + +# One scenario, real proving (slow) +cargo run --release -p integration_bench -- --scenario amm +``` + +Scenarios: `token`, `amm`, `fanout`, `private`, `parallel`, `all`. + +All scenarios share a single TestContext for the run (one Bedrock + sequencer + indexer + wallet across the whole run, chain state accumulating), which matches how the node runs in production. + +## What you'll see + +Per scenario: a step table (`submit_s`, `inclusion_s`, `sync_s`, `total_s`) and a size summary covering every block captured during the scenario (block_bytes total/mean/min/max; per-tx-variant sizes for public, PPE, and program-deployment transactions). + +The fanout, parallel, and private scenarios are the most representative for L1-payload-size measurements since they put multiple txs per block. + +JSON output is written to `target/integration_bench_dev.json` (dev mode) or `target/integration_bench_prove.json` (real proving). diff --git a/tools/e2e_bench/src/harness.rs b/tools/integration_bench/src/harness.rs similarity index 95% rename from tools/e2e_bench/src/harness.rs rename to tools/integration_bench/src/harness.rs index 7020352b..b00351b9 100644 --- a/tools/e2e_bench/src/harness.rs +++ b/tools/integration_bench/src/harness.rs @@ -11,10 +11,9 @@ use anyhow::{Result, bail}; use common::transaction::NSSATransaction; use sequencer_service_rpc::RpcClient as _; use serde::{Serialize, Serializer}; +use test_fixtures::{DiskSizes, TestContext}; use wallet::cli::SubcommandReturnValue; -use crate::bench_context::BenchContext; - const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250); const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120); @@ -51,15 +50,13 @@ pub struct StepResult { #[derive(Debug, Serialize, Default)] pub struct ScenarioOutput { pub name: String, - #[serde(serialize_with = "ser_duration_secs", rename = "setup_s")] - pub setup: Duration, pub steps: Vec, #[serde(serialize_with = "ser_duration_secs", rename = "total_s")] pub total: Duration, /// Disk sizes (sequencer / indexer / wallet tempdirs) sampled at scenario start. - pub disk_before: Option, + pub disk_before: Option, /// Disk sizes sampled at scenario end. - pub disk_after: Option, + pub disk_after: Option, /// Bedrock-finality latency: time from final-step inclusion to the indexer /// reporting the sequencer tip as L1-finalised. Effectively measures the /// sequencer→Bedrock posting + Bedrock finalisation + indexer L1 ingest path. @@ -85,7 +82,7 @@ impl ScenarioOutput { /// Begin a timed step. Capture this *before* submitting the wallet operation /// so we can later subtract it from the post-submit block height to detect /// when the chain has advanced past the tx's block. -pub async fn begin_step(ctx: &BenchContext) -> Result { +pub async fn begin_step(ctx: &TestContext) -> Result { Ok(ctx.sequencer_client().get_last_block_id().await?) } @@ -105,7 +102,7 @@ pub async fn finalize_step( started: Instant, pre_block_id: u64, ret: &SubcommandReturnValue, - ctx: &mut BenchContext, + ctx: &mut TestContext, ) -> Result { let label = label.into(); let submit = started.elapsed(); @@ -174,7 +171,7 @@ pub async fn finalize_step( /// Wait for `get_last_block_id` to advance by at least `min_blocks` from `from_block_id`. pub async fn wait_for_chain_advance( - ctx: &BenchContext, + ctx: &TestContext, from_block_id: u64, min_blocks: u64, ) -> Result<()> { @@ -197,7 +194,7 @@ pub async fn wait_for_chain_advance( } } -async fn sync_wallet_to_tip(ctx: &mut BenchContext) -> Result<()> { +async fn sync_wallet_to_tip(ctx: &mut TestContext) -> Result<()> { let last_block = ctx.sequencer_client().get_last_block_id().await?; ctx.wallet_mut().sync_to_block(last_block).await?; Ok(()) @@ -213,9 +210,8 @@ pub fn print_table(output: &ScenarioOutput) { .max("step".len()); println!( - "\nScenario: {} (setup {:.2}s, total {:.2}s)", + "\nScenario: {} (total {:.2}s)", output.name, - output.setup.as_secs_f64(), output.total.as_secs_f64(), ); println!( diff --git a/tools/e2e_bench/src/main.rs b/tools/integration_bench/src/main.rs similarity index 60% rename from tools/e2e_bench/src/main.rs rename to tools/integration_bench/src/main.rs index 7de1d323..b218a087 100644 --- a/tools/e2e_bench/src/main.rs +++ b/tools/integration_bench/src/main.rs @@ -1,18 +1,18 @@ //! End-to-end LEZ scenario bench. //! -//! Spins up the full stack (native Bedrock node launched per-scenario via -//! `BedrockHandle` + in-process sequencer + indexer + wallet via -//! `BenchContext`) and drives the wallet through configurable scenarios that -//! mirror real user flows. Times each step and records borsh-serialized -//! block + tx sizes per scenario. +//! Spins up the full stack via `test_fixtures::TestContext` (docker-compose +//! Bedrock + in-process sequencer + indexer + wallet) once for the whole run, +//! then drives the wallet through each requested scenario against that single +//! shared stack. Times each step and records borsh-serialized block + tx sizes +//! per scenario. //! -//! Required env vars (no defaults; see `tools/e2e_bench/README.md`): -//! `LEZ_BEDROCK_BIN` absolute path to logos-blockchain-node. -//! `LEZ_BEDROCK_CONFIG_DIR` directory with node-config.yaml + deployment template. +//! Prerequisite: a working local Docker daemon. The Bedrock service is brought +//! up via the same `bedrock/docker-compose.yml` the integration tests use, so +//! no host-side binary or env vars are required. //! //! Run examples: -//! `RISC0_DEV_MODE=1` `cargo run --release -p e2e_bench -- --scenario all`. -//! `cargo run --release -p e2e_bench -- --scenario amm`. +//! `RISC0_DEV_MODE=1 cargo run --release -p integration_bench -- --scenario all`. +//! `cargo run --release -p integration_bench -- --scenario amm`. //! //! `RISC0_DEV_MODE=1` skips proving and produces latency-only numbers in //! ~minutes; omitting it produces realistic proving-inclusive numbers but @@ -31,14 +31,11 @@ use std::{path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; -use bedrock_handle::BedrockHandle; -use bench_context::BenchContext; use clap::{Parser, ValueEnum}; use harness::ScenarioOutput; use serde::Serialize; +use test_fixtures::TestContext; -mod bedrock_handle; -mod bench_context; mod harness; mod scenarios; @@ -59,7 +56,7 @@ struct Cli { #[arg(long, value_enum, default_value_t = ScenarioName::All)] scenario: ScenarioName, - /// Optional JSON output path. Defaults to `/target/e2e_bench.json`. + /// Optional JSON output path. Defaults to `/target/integration_bench.json`. #[arg(long)] json_out: Option, } @@ -67,20 +64,24 @@ struct Cli { #[derive(Debug, Serialize)] struct BenchRunReport { risc0_dev_mode: bool, + /// Time to bring up the shared `TestContext` (docker-compose Bedrock + + /// sequencer + indexer + wallet). Paid once per run regardless of how many + /// scenarios are exercised. + shared_setup_s: f64, scenarios: Vec, total_wall_s: f64, } #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { - // integration_tests initializes env_logger via a LazyLock, so we leave logger + // test_fixtures initializes env_logger via a LazyLock, so we leave logger // setup to it. Set RUST_LOG=info before running to see logs. let cli = Cli::parse(); let risc0_dev_mode = std::env::var("RISC0_DEV_MODE").is_ok_and(|v| !v.is_empty() && v != "0"); eprintln!( - "e2e_bench: scenario={:?}, RISC0_DEV_MODE={}", + "integration_bench: scenario={:?}, RISC0_DEV_MODE={}", cli.scenario, if risc0_dev_mode { "1" } else { "unset/0" } ); @@ -97,43 +98,28 @@ async fn main() -> Result<()> { }; let overall_started = std::time::Instant::now(); + + // One shared stack for the entire run: docker-compose Bedrock + sequencer + + // indexer + wallet. Scenarios share chain state, which matches how the node + // runs in production (long-lived, accumulating). + let setup_started = std::time::Instant::now(); + let mut ctx = TestContext::new() + .await + .context("failed to setup TestContext")?; + let shared_setup = setup_started.elapsed(); + eprintln!("setup: {:.2}s", shared_setup.as_secs_f64()); + let mut all_outputs = Vec::with_capacity(to_run.len()); for name in to_run { eprintln!("\n=== running scenario: {name:?} ==="); - { - let setup_started = std::time::Instant::now(); - // Spawn a fresh Bedrock node for this scenario. Each scenario therefore - // starts with an empty chain so the indexer never has a backlog from a - // prior scenario. - let bedrock = BedrockHandle::launch_fresh() - .await - .with_context(|| format!("failed to spawn Bedrock for scenario {name:?}"))?; - let bedrock_addr_string = format!("{}", bedrock.addr()); - // SAFETY: env::set_var happens before any threaded setup that reads env. - unsafe { - std::env::set_var("LEZ_BEDROCK_ADDR", &bedrock_addr_string); - } - - let mut ctx = BenchContext::new() - .await - .with_context(|| format!("failed to setup BenchContext for scenario {name:?}"))?; - let setup = setup_started.elapsed(); - eprintln!("setup: {:.2}s", setup.as_secs_f64()); - - let disk_before = ctx.disk_sizes(); - let mut output = run_scenario(name, setup, &mut ctx).await?; - output.disk_before = Some(disk_before); - output.disk_after = Some(ctx.disk_sizes()); - output.bedrock_finality = Some(measure_bedrock_finality(&ctx).await?); - harness::print_table(&output); - all_outputs.push(output); - - // ctx and bedrock drop here at end of scope, killing the bedrock child - // before we sleep so the next iteration can rebind the port. - } - // Give Bedrock a moment to shut down before the next scenario. - tokio::time::sleep(Duration::from_secs(2)).await; + let disk_before = ctx.disk_sizes(); + let mut output = run_scenario(name, &mut ctx).await?; + output.disk_before = Some(disk_before); + output.disk_after = Some(ctx.disk_sizes()); + output.bedrock_finality = Some(measure_bedrock_finality(&ctx).await?); + harness::print_table(&output); + all_outputs.push(output); } let total_wall_s = overall_started.elapsed().as_secs_f64(); @@ -141,6 +127,7 @@ async fn main() -> Result<()> { let report = BenchRunReport { risc0_dev_mode, + shared_setup_s: shared_setup.as_secs_f64(), scenarios: all_outputs, total_wall_s, }; @@ -155,7 +142,7 @@ async fn main() -> Result<()> { let suffix = if risc0_dev_mode { "dev" } else { "prove" }; workspace_root .join("target") - .join(format!("e2e_bench_{suffix}.json")) + .join(format!("integration_bench_{suffix}.json")) }; if let Some(parent) = out_path.parent() { std::fs::create_dir_all(parent)?; @@ -166,26 +153,21 @@ async fn main() -> Result<()> { Ok(()) } -async fn run_scenario( - name: ScenarioName, - setup: Duration, - ctx: &mut BenchContext, -) -> Result { - let output = match name { - ScenarioName::Token => scenarios::token::run(ctx).await?, - ScenarioName::Amm => scenarios::amm::run(ctx).await?, - ScenarioName::Fanout => scenarios::fanout::run(ctx).await?, - ScenarioName::Private => scenarios::private::run(ctx).await?, - ScenarioName::Parallel => scenarios::parallel::run(ctx).await?, +async fn run_scenario(name: ScenarioName, ctx: &mut TestContext) -> Result { + match name { + ScenarioName::Token => scenarios::token::run(ctx).await, + ScenarioName::Amm => scenarios::amm::run(ctx).await, + ScenarioName::Fanout => scenarios::fanout::run(ctx).await, + ScenarioName::Private => scenarios::private::run(ctx).await, + ScenarioName::Parallel => scenarios::parallel::run(ctx).await, ScenarioName::All => unreachable!("dispatched above"), - }; - Ok(ScenarioOutput { setup, ..output }) + } } /// Poll the indexer's L1-finalised block id until it catches up with the /// sequencer's last block id. This is effectively the sequencer→Bedrock posting /// plus Bedrock finalisation plus indexer ingest latency. -async fn measure_bedrock_finality(ctx: &BenchContext) -> Result { +async fn measure_bedrock_finality(ctx: &TestContext) -> Result { use indexer_service_rpc::RpcClient as _; use jsonrpsee::ws_client::WsClientBuilder; use sequencer_service_rpc::RpcClient as _; diff --git a/tools/e2e_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs similarity index 96% rename from tools/e2e_bench/src/scenarios/amm.rs rename to tools/integration_bench/src/scenarios/amm.rs index f295a4aa..d0ddd6f0 100644 --- a/tools/e2e_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -12,7 +12,7 @@ use wallet::cli::{ use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { +pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { let mut output = ScenarioOutput::new("amm_swap_flow"); let def_a = new_public_account(ctx, &mut output, "create_acc_def_a").await?; @@ -125,7 +125,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result { @@ -148,7 +148,7 @@ async fn new_public_account( } async fn timed_token_new( - ctx: &mut crate::bench_context::BenchContext, + ctx: &mut test_fixtures::TestContext, output: &mut ScenarioOutput, label: &str, def_id: nssa::AccountId, @@ -173,7 +173,7 @@ async fn timed_token_new( } async fn timed_token_send( - ctx: &mut crate::bench_context::BenchContext, + ctx: &mut test_fixtures::TestContext, output: &mut ScenarioOutput, label: &str, from_id: nssa::AccountId, diff --git a/tools/e2e_bench/src/scenarios/fanout.rs b/tools/integration_bench/src/scenarios/fanout.rs similarity index 95% rename from tools/e2e_bench/src/scenarios/fanout.rs rename to tools/integration_bench/src/scenarios/fanout.rs index 6f85a974..fc5235b5 100644 --- a/tools/e2e_bench/src/scenarios/fanout.rs +++ b/tools/integration_bench/src/scenarios/fanout.rs @@ -15,7 +15,7 @@ use crate::harness::{ScenarioOutput, finalize_step}; const FANOUT_COUNT: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { +pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { let mut output = ScenarioOutput::new("multi_recipient_fanout"); let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; @@ -67,7 +67,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result { diff --git a/tools/e2e_bench/src/scenarios/mod.rs b/tools/integration_bench/src/scenarios/mod.rs similarity index 100% rename from tools/e2e_bench/src/scenarios/mod.rs rename to tools/integration_bench/src/scenarios/mod.rs diff --git a/tools/e2e_bench/src/scenarios/parallel.rs b/tools/integration_bench/src/scenarios/parallel.rs similarity index 96% rename from tools/e2e_bench/src/scenarios/parallel.rs rename to tools/integration_bench/src/scenarios/parallel.rs index 29c3a72f..7ae03a09 100644 --- a/tools/e2e_bench/src/scenarios/parallel.rs +++ b/tools/integration_bench/src/scenarios/parallel.rs @@ -7,23 +7,20 @@ use std::time::Instant; use anyhow::{Result, bail}; use common::transaction::NSSATransaction; -use test_fixtures::public_mention; use sequencer_service_rpc::RpcClient as _; +use test_fixtures::{TestContext, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, programs::token::TokenProgramAgnosticSubcommand, }; -use crate::{ - bench_context::BenchContext, - harness::{BlockSize, ScenarioOutput, StepResult, finalize_step}, -}; +use crate::harness::{BlockSize, ScenarioOutput, StepResult, finalize_step}; const PARALLEL_FANOUT_N: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; -pub async fn run(ctx: &mut BenchContext) -> Result { +pub async fn run(ctx: &mut TestContext) -> Result { let mut output = ScenarioOutput::new("parallel_fanout"); // Setup: definition, master supply, N parallel supplies, N recipients. @@ -168,7 +165,7 @@ pub async fn run(ctx: &mut BenchContext) -> Result { } async fn new_public_account( - ctx: &mut BenchContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { diff --git a/tools/e2e_bench/src/scenarios/private.rs b/tools/integration_bench/src/scenarios/private.rs similarity index 96% rename from tools/e2e_bench/src/scenarios/private.rs rename to tools/integration_bench/src/scenarios/private.rs index f0b4745b..54dcab2b 100644 --- a/tools/e2e_bench/src/scenarios/private.rs +++ b/tools/integration_bench/src/scenarios/private.rs @@ -12,7 +12,7 @@ use wallet::cli::{ use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { +pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { let mut output = ScenarioOutput::new("private_chained_flow"); let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; @@ -104,7 +104,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result { @@ -127,7 +127,7 @@ async fn new_public_account( } async fn new_private_account( - ctx: &mut crate::bench_context::BenchContext, + ctx: &mut test_fixtures::TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { diff --git a/tools/e2e_bench/src/scenarios/token.rs b/tools/integration_bench/src/scenarios/token.rs similarity index 95% rename from tools/e2e_bench/src/scenarios/token.rs rename to tools/integration_bench/src/scenarios/token.rs index 4e63da32..235394e0 100644 --- a/tools/e2e_bench/src/scenarios/token.rs +++ b/tools/integration_bench/src/scenarios/token.rs @@ -12,7 +12,7 @@ use wallet::cli::{ use crate::harness::{ScenarioOutput, finalize_step}; -pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result { +pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { let mut output = ScenarioOutput::new("token_onboarding"); let definition_id = new_public_account(ctx, &mut output, "create_pub_definition").await?; @@ -81,7 +81,7 @@ pub async fn run(ctx: &mut crate::bench_context::BenchContext) -> Result Result { @@ -104,7 +104,7 @@ async fn new_public_account( } async fn new_private_account( - ctx: &mut crate::bench_context::BenchContext, + ctx: &mut test_fixtures::TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { From ab77c5d26a8115420a1fd678df8d814f4550a61b Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 20 May 2026 12:19:43 +0200 Subject: [PATCH 08/11] refactor(integration_bench): ScenarioOutput::step closure helper --- tools/integration_bench/src/harness.rs | 28 +- tools/integration_bench/src/main.rs | 4 +- tools/integration_bench/src/scenarios/amm.rs | 243 ++++++++---------- .../integration_bench/src/scenarios/fanout.rs | 96 ++++--- .../src/scenarios/parallel.rs | 94 +++---- .../src/scenarios/private.rs | 184 +++++++------ .../integration_bench/src/scenarios/token.rs | 152 ++++++----- 7 files changed, 383 insertions(+), 418 deletions(-) diff --git a/tools/integration_bench/src/harness.rs b/tools/integration_bench/src/harness.rs index b00351b9..9337f3ed 100644 --- a/tools/integration_bench/src/harness.rs +++ b/tools/integration_bench/src/harness.rs @@ -77,12 +77,29 @@ impl ScenarioOutput { self.total = self.total.saturating_add(step.total); self.steps.push(step); } + + /// Run a single timed step against `ctx`: capture pre-block, run `submit`, + /// finalize timings, push a `StepResult` onto `self.steps`. Returns the + /// `SubcommandReturnValue` from `submit` so the caller can match on it. + pub async fn step( + &mut self, + ctx: &mut TestContext, + label: impl Into, + submit: impl AsyncFnOnce(&mut TestContext) -> Result, + ) -> Result { + let pre_block = begin_step(ctx).await?; + let started = Instant::now(); + let ret = submit(ctx).await?; + let step = finalize_step(label, started, pre_block, &ret, ctx).await?; + self.push(step); + Ok(ret) + } } /// Begin a timed step. Capture this *before* submitting the wallet operation /// so we can later subtract it from the post-submit block height to detect /// when the chain has advanced past the tx's block. -pub async fn begin_step(ctx: &TestContext) -> Result { +async fn begin_step(ctx: &TestContext) -> Result { Ok(ctx.sequencer_client().get_last_block_id().await?) } @@ -90,14 +107,7 @@ pub async fn begin_step(ctx: &TestContext) -> Result { /// being captured and `ret` being received) and, if `ret` is a /// [`SubcommandReturnValue::PrivacyPreservingTransfer`], polls the sequencer /// for inclusion and records the inclusion latency. Returns a [`StepResult`]. -/// -/// Usage: -/// ```ignore -/// let started = Instant::now(); -/// let ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), cmd).await?; -/// let step = finalize_step("label", started, pre_block_id, &ret, ctx).await?; -/// ``` -pub async fn finalize_step( +async fn finalize_step( label: impl Into, started: Instant, pre_block_id: u64, diff --git a/tools/integration_bench/src/main.rs b/tools/integration_bench/src/main.rs index b218a087..ccf7058e 100644 --- a/tools/integration_bench/src/main.rs +++ b/tools/integration_bench/src/main.rs @@ -22,10 +22,12 @@ clippy::arithmetic_side_effects, clippy::print_stderr, clippy::print_stdout, + clippy::shadow_unrelated, clippy::wildcard_enum_match_arm, reason = "Bench tool: stderr/stdout output is the deliverable; small Duration / iterator-sum \ arithmetic is safe at bench scale; bench scenarios bail loudly on any unexpected \ - return variant, which is preferable to maintaining an exhaustive list in five files." + return variant, which is preferable to maintaining an exhaustive list in five files; \ + the step() closure helper canonically rebinds `ctx` inside the closure body." )] use std::{path::PathBuf, time::Duration}; diff --git a/tools/integration_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs index d0ddd6f0..f05eed1a 100644 --- a/tools/integration_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -1,18 +1,16 @@ //! AMM swap flow: setup two tokens, create pool, swap, add liquidity, remove liquidity. -use std::time::Instant; - use anyhow::{Result, bail}; -use test_fixtures::public_mention; +use test_fixtures::{TestContext, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, }; -use crate::harness::{ScenarioOutput, finalize_step}; +use crate::harness::ScenarioOutput; -pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { +pub async fn run(ctx: &mut TestContext) -> Result { let mut output = ScenarioOutput::new("amm_swap_flow"); let def_a = new_public_account(ctx, &mut output, "create_acc_def_a").await?; @@ -26,121 +24,97 @@ pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result let user_lp = new_public_account(ctx, &mut output, "create_acc_user_lp").await?; timed_token_new(ctx, &mut output, "token_a_new", def_a, supply_a, "TokA").await?; - timed_token_send( - ctx, - &mut output, - "token_a_fund_user", - supply_a, - user_a, - 1_000, - ) - .await?; + timed_token_send(ctx, &mut output, "token_a_fund_user", supply_a, user_a, 1_000).await?; timed_token_new(ctx, &mut output, "token_b_new", def_b, supply_b, "TokB").await?; - timed_token_send( - ctx, - &mut output, - "token_b_fund_user", - supply_b, - user_b, - 1_000, - ) - .await?; + timed_token_send(ctx, &mut output, "token_b_fund_user", supply_b, user_b, 1_000).await?; - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::AMM(AmmProgramAgnosticSubcommand::New { - user_holding_a: public_mention(user_a), - user_holding_b: public_mention(user_b), - user_holding_lp: public_mention(user_lp), - balance_a: 300, - balance_b: 300, - }), - ) + output + .step(ctx, "amm_new_pool", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::New { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + balance_a: 300, + balance_b: 300, + }), + ) + .await + }) .await?; - let step = finalize_step("amm_new_pool", started, pre_block, &ret, ctx).await?; - output.push(step); - } - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::AMM(AmmProgramAgnosticSubcommand::SwapExactInput { - user_holding_a: public_mention(user_a), - user_holding_b: public_mention(user_b), - amount_in: 50, - min_amount_out: 1, - token_definition: def_a, - }), - ) + output + .step(ctx, "amm_swap_exact_input", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::SwapExactInput { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + amount_in: 50, + min_amount_out: 1, + token_definition: def_a, + }), + ) + .await + }) .await?; - let step = finalize_step("amm_swap_exact_input", started, pre_block, &ret, ctx).await?; - output.push(step); - } - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::AMM(AmmProgramAgnosticSubcommand::AddLiquidity { - user_holding_a: public_mention(user_a), - user_holding_b: public_mention(user_b), - user_holding_lp: public_mention(user_lp), - min_amount_lp: 1, - max_amount_a: 100, - max_amount_b: 100, - }), - ) + output + .step(ctx, "amm_add_liquidity", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::AddLiquidity { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + min_amount_lp: 1, + max_amount_a: 100, + max_amount_b: 100, + }), + ) + .await + }) .await?; - let step = finalize_step("amm_add_liquidity", started, pre_block, &ret, ctx).await?; - output.push(step); - } - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::AMM(AmmProgramAgnosticSubcommand::RemoveLiquidity { - user_holding_a: public_mention(user_a), - user_holding_b: public_mention(user_b), - user_holding_lp: public_mention(user_lp), - balance_lp: 50, - min_amount_a: 1, - min_amount_b: 1, - }), - ) + output + .step(ctx, "amm_remove_liquidity", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::AMM(AmmProgramAgnosticSubcommand::RemoveLiquidity { + user_holding_a: public_mention(user_a), + user_holding_b: public_mention(user_b), + user_holding_lp: public_mention(user_lp), + balance_lp: 50, + min_amount_a: 1, + min_amount_b: 1, + }), + ) + .await + }) .await?; - let step = finalize_step("amm_remove_liquidity", started, pre_block, &ret, ctx).await?; - output.push(step); - } Ok(output) } async fn new_public_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -148,53 +122,54 @@ async fn new_public_account( } async fn timed_token_new( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, def_id: nssa::AccountId, supply_id: nssa::AccountId, name: &str, ) -> Result<()> { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(def_id), - supply_account_id: public_mention(supply_id), - name: name.to_owned(), - total_supply: 10_000, - }), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let name = name.to_owned(); + output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name, + total_supply: 10_000, + }), + ) + .await + }) + .await?; Ok(()) } async fn timed_token_send( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, from_id: nssa::AccountId, to_id: nssa::AccountId, amount: u128, ) -> Result<()> { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(from_id), - to: Some(public_mention(to_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount, - }), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(from_id), + to: Some(public_mention(to_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount, + }), + ) + .await + }) + .await?; Ok(()) } diff --git a/tools/integration_bench/src/scenarios/fanout.rs b/tools/integration_bench/src/scenarios/fanout.rs index fc5235b5..d03adf83 100644 --- a/tools/integration_bench/src/scenarios/fanout.rs +++ b/tools/integration_bench/src/scenarios/fanout.rs @@ -1,42 +1,38 @@ //! Multi-recipient fanout: one funded supply pays 10 distinct recipients. -use std::time::Instant; - use anyhow::{Result, bail}; -use test_fixtures::public_mention; +use test_fixtures::{TestContext, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioOutput, finalize_step}; +use crate::harness::ScenarioOutput; const FANOUT_COUNT: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; -pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { +pub async fn run(ctx: &mut TestContext) -> Result { let mut output = ScenarioOutput::new("multi_recipient_fanout"); let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; let supply_id = new_public_account(ctx, &mut output, "create_acc_supply").await?; - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(def_id), - supply_account_id: public_mention(supply_id), - name: "FanoutToken".to_owned(), - total_supply: 10_000_000, - }), - ) + output + .step(ctx, "token_new_fungible", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name: "FanoutToken".to_owned(), + total_supply: 10_000_000, + }), + ) + .await + }) .await?; - let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; - output.push(step); - } let mut recipients = Vec::with_capacity(FANOUT_COUNT); for i in 0..FANOUT_COUNT { @@ -44,45 +40,45 @@ pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result recipients.push(id); } - for (i, recipient_id) in recipients.iter().enumerate() { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_id), - to: Some(public_mention(*recipient_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: AMOUNT_PER_TRANSFER, - }), - ) - .await?; - let step = finalize_step(format!("transfer_{i:02}"), started, pre_block, &ret, ctx).await?; - output.push(step); + for (i, recipient_id) in recipients.iter().copied().enumerate() { + output + .step(ctx, format!("transfer_{i:02}"), async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(public_mention(recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: AMOUNT_PER_TRANSFER, + }), + ) + .await + }) + .await?; } Ok(output) } async fn new_public_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/integration_bench/src/scenarios/parallel.rs b/tools/integration_bench/src/scenarios/parallel.rs index 7ae03a09..c6a265b9 100644 --- a/tools/integration_bench/src/scenarios/parallel.rs +++ b/tools/integration_bench/src/scenarios/parallel.rs @@ -15,7 +15,7 @@ use wallet::cli::{ programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{BlockSize, ScenarioOutput, StepResult, finalize_step}; +use crate::harness::{BlockSize, ScenarioOutput, StepResult}; const PARALLEL_FANOUT_N: usize = 10; const AMOUNT_PER_TRANSFER: u128 = 100; @@ -43,47 +43,47 @@ pub async fn run(ctx: &mut TestContext) -> Result { .expect("usize fits u128") .saturating_mul(AMOUNT_PER_TRANSFER) .saturating_mul(10); - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(def_id), - supply_account_id: public_mention(master_id), - name: "ParToken".to_owned(), - total_supply: total_mint, - }), - ) + output + .step(ctx, "token_new_fungible", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(master_id), + name: "ParToken".to_owned(), + total_supply: total_mint, + }), + ) + .await + }) .await?; - let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; - output.push(step); - } // Fund each sender from master. Serial; this is setup, not measured throughput. - for (i, sender_id) in senders.iter().enumerate() { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(master_id), - to: Some(public_mention(*sender_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: AMOUNT_PER_TRANSFER * 5, - }), - ) - .await?; - let step = - finalize_step(format!("fund_sender_{i:02}"), started, pre_block, &ret, ctx).await?; - output.push(step); + for (i, sender_id) in senders.iter().copied().enumerate() { + output + .step(ctx, format!("fund_sender_{i:02}"), async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(master_id), + to: Some(public_mention(sender_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: AMOUNT_PER_TRANSFER * 5, + }), + ) + .await + }) + .await?; } // The measured phase: submit N transfers as fast as possible, do not wait // for chain advance between submits. The sequencer batches whatever lands in - // its mempool before block_create_timeout. + // its mempool before block_create_timeout. The burst step is captured + // manually rather than via the `step()` helper because we need to time + // submit-and-inclusion as two separate intervals over a synthesised batch + // rather than per-tx. let pre_block_burst = ctx.sequencer_client().get_last_block_id().await?; let burst_started = Instant::now(); @@ -169,18 +169,18 @@ async fn new_public_account( output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/integration_bench/src/scenarios/private.rs b/tools/integration_bench/src/scenarios/private.rs index 54dcab2b..2be8c43c 100644 --- a/tools/integration_bench/src/scenarios/private.rs +++ b/tools/integration_bench/src/scenarios/private.rs @@ -1,18 +1,16 @@ //! Private chained flow: shielded, deshielded, and private-to-private transfers. -use std::time::Instant; - use anyhow::{Result, bail}; -use test_fixtures::{private_mention, public_mention}; +use test_fixtures::{TestContext, private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioOutput, finalize_step}; +use crate::harness::ScenarioOutput; -pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { +pub async fn run(ctx: &mut TestContext) -> Result { let mut output = ScenarioOutput::new("private_chained_flow"); let def_id = new_public_account(ctx, &mut output, "create_acc_def").await?; @@ -23,103 +21,95 @@ pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result let private_b = new_private_account(ctx, &mut output, "create_acc_priv_b").await?; // Mint into public supply. - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(def_id), - supply_account_id: public_mention(supply_id), - name: "PrivToken".to_owned(), - total_supply: 1_000_000, - }), - ) + output + .step(ctx, "token_new_fungible", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(def_id), + supply_account_id: public_mention(supply_id), + name: "PrivToken".to_owned(), + total_supply: 1_000_000, + }), + ) + .await + }) .await?; - let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; - output.push(step); - } // Shielded transfer: public supply -> private_a. - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_id), - to: Some(private_mention(private_a)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: 1_000, - }), - ) + output + .step(ctx, "shielded_transfer", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(private_mention(private_a)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 1_000, + }), + ) + .await + }) .await?; - let step = finalize_step("shielded_transfer", started, pre_block, &ret, ctx).await?; - output.push(step); - } // Deshielded transfer: private_a -> public_recipient. - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: private_mention(private_a), - to: Some(public_mention(public_recipient_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: 100, - }), - ) + output + .step(ctx, "deshielded_transfer", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: private_mention(private_a), + to: Some(public_mention(public_recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 100, + }), + ) + .await + }) .await?; - let step = finalize_step("deshielded_transfer", started, pre_block, &ret, ctx).await?; - output.push(step); - } // Private-to-private transfer: private_a -> private_b. - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: private_mention(private_a), - to: Some(private_mention(private_b)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: 200, - }), - ) + output + .step(ctx, "private_to_private", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: private_mention(private_a), + to: Some(private_mention(private_b)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 200, + }), + ) + .await + }) .await?; - let step = finalize_step("private_to_private", started, pre_block, &ret, ctx).await?; - output.push(step); - } Ok(output) } async fn new_public_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -127,22 +117,22 @@ async fn new_public_account( } async fn new_private_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), diff --git a/tools/integration_bench/src/scenarios/token.rs b/tools/integration_bench/src/scenarios/token.rs index 235394e0..d1dfdef3 100644 --- a/tools/integration_bench/src/scenarios/token.rs +++ b/tools/integration_bench/src/scenarios/token.rs @@ -1,102 +1,94 @@ //! Token onboarding scenario: create accounts, mint, public transfer, private transfer. -use std::time::Instant; - use anyhow::{Result, bail}; -use test_fixtures::{private_mention, public_mention}; +use test_fixtures::{TestContext, private_mention, public_mention}; use wallet::cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, programs::token::TokenProgramAgnosticSubcommand, }; -use crate::harness::{ScenarioOutput, finalize_step}; +use crate::harness::ScenarioOutput; -pub async fn run(ctx: &mut test_fixtures::TestContext) -> Result { +pub async fn run(ctx: &mut TestContext) -> Result { let mut output = ScenarioOutput::new("token_onboarding"); let definition_id = new_public_account(ctx, &mut output, "create_pub_definition").await?; let supply_id = new_public_account(ctx, &mut output, "create_pub_supply").await?; let recipient_id = new_public_account(ctx, &mut output, "create_pub_recipient").await?; - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_id), - supply_account_id: public_mention(supply_id), - name: "BenchToken".to_owned(), - total_supply: 1_000_000, - }), - ) + output + .step(ctx, "token_new_fungible", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::New { + definition_account_id: public_mention(definition_id), + supply_account_id: public_mention(supply_id), + name: "BenchToken".to_owned(), + total_supply: 1_000_000, + }), + ) + .await + }) .await?; - let step = finalize_step("token_new_fungible", started, pre_block, &ret, ctx).await?; - output.push(step); - } - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_id), - to: Some(public_mention(recipient_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: 1_000, - }), - ) + output + .step(ctx, "token_public_transfer", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(public_mention(recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 1_000, + }), + ) + .await + }) .await?; - let step = finalize_step("token_public_transfer", started, pre_block, &ret, ctx).await?; - output.push(step); - } let private_recipient_id = new_private_account(ctx, &mut output, "create_priv_recipient").await?; - { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_id), - to: Some(private_mention(private_recipient_id)), - to_npk: None, - to_vpk: None, - to_identifier: Some(0), - amount: 500, - }), - ) + output + .step(ctx, "token_shielded_transfer", async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Token(TokenProgramAgnosticSubcommand::Send { + from: public_mention(supply_id), + to: Some(private_mention(private_recipient_id)), + to_npk: None, + to_vpk: None, + to_identifier: Some(0), + amount: 500, + }), + ) + .await + }) .await?; - let step = finalize_step("token_shielded_transfer", started, pre_block, &ret, ctx).await?; - output.push(step); - } Ok(output) } async fn new_public_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), @@ -104,22 +96,22 @@ async fn new_public_account( } async fn new_private_account( - ctx: &mut test_fixtures::TestContext, + ctx: &mut TestContext, output: &mut ScenarioOutput, label: &str, ) -> Result { - let pre_block = crate::harness::begin_step(ctx).await?; - let started = Instant::now(); - let ret = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let step = finalize_step(label, started, pre_block, &ret, ctx).await?; - output.push(step); + let ret = output + .step(ctx, label, async |ctx| { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: None, + })), + ) + .await + }) + .await?; match ret { SubcommandReturnValue::RegisterAccount { account_id } => Ok(account_id), other => bail!("expected RegisterAccount, got {other:?}"), From b0a5b3478b9c635f76b77446f09454e9e8bac406 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 20 May 2026 12:58:25 +0200 Subject: [PATCH 09/11] docs(integration_bench): add canonical run numbers from docker-compose sweep --- docs/benchmarks/integration_bench.md | 65 +++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/docs/benchmarks/integration_bench.md b/docs/benchmarks/integration_bench.md index a1295dec..dd7bfe6e 100644 --- a/docs/benchmarks/integration_bench.md +++ b/docs/benchmarks/integration_bench.md @@ -2,7 +2,7 @@ End-to-end LEZ scenarios driven through the wallet against a docker-compose Bedrock node + in-process sequencer + indexer (via `test_fixtures::TestContext`). Times each step and records borsh sizes per block, split by tx variant. -No numeric tables here yet. Absolute wall time and block sizes depend heavily on the bedrock config (block cadence and confirmation depth) and on dev-mode vs real proving; re-run the bench locally to get numbers for your own setup. Canonical numbers will be added once the bench runs against the standard configuration. +Numbers below are from a single-host docker-compose run on an Apple M2 Pro (CPU only, no GPU acceleration). Absolute wall time and block sizes depend heavily on the bedrock config (block cadence and confirmation depth) and on dev-mode vs real proving; re-run the bench locally to characterise your own setup. ## Scenarios @@ -27,12 +27,73 @@ No numeric tables here yet. Absolute wall time and block sizes depend heavily on | `bedrock_finality_s` | same in both modes | same in both modes (L1 cadence, not LEZ prover) | | Blocks captured | similar in both modes | real captures more empty clock-only ticks that fill prove wall-time | -Numbers are intentionally omitted in this document until the canonical run lands. Public-only scenarios converge between modes within run-to-run jitter; the qualitative differences are captured by the table above. +Tables below report dev-mode for all five scenarios. Real-proving numbers are included for `amm_swap_flow` (representative all-public) and `private_chained_flow` (representative chained-private flow); public-only scenarios converge between modes within run-to-run jitter, so a full real-proving sweep is not run here. ## Methodology Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. Empty clock-only ticks give the per-block fixed-cost baseline. Wall time is captured per step (submit + inclusion + wallet sync) and aggregated to the per-scenario `total_s`. The one-time stack-setup cost (`shared_setup_s` at the run level) and the closing bedrock finality wait (`bedrock_finality_s` per scenario) are reported separately, not folded into `total_s`. +## Step latencies — dev mode (`RISC0_DEV_MODE=1`) + +Per-scenario wall time and Bedrock L1-finality latency for the closing tip. + +| Scenario | total_s | bedrock_finality_s | +|---|---:|---:| +| token_onboarding | 61.36 | 5.88 | +| amm_swap_flow | 156.50 | 27.99 | +| multi_recipient_fanout | 214.40 | 31.71 | +| private_chained_flow | 109.31 | 8.73 | +| parallel_fanout | 234.42 | 20.29 | + +Shared TestContext setup: 139.80 s (paid once per run). Total dev-mode wall time across all five scenarios: 1010.4 s. + +## Step latencies — real proving (selected scenarios) + +| Scenario | total_s | bedrock_finality_s | Δ vs dev | +|---|---:|---:|---:| +| amm_swap_flow | 156.20 | 26.95 | ~0 (all-public) | +| private_chained_flow | 391.74 | 9.40 | +282.4 s (≈ 94 s per PPE step × 3) | + +Per-step breakdown for `private_chained_flow` in real proving: + +| Step | submit_s | inclusion_s | total_s | +|---|---:|---:|---:| +| token_new_fungible (public) | 0.003 | 10.857 | 11.006 | +| shielded_transfer (PPE) | 125.416 | 0.001 | 125.469 | +| deshielded_transfer (PPE) | 126.261 | 0.001 | 126.311 | +| private_to_private (PPE) | 128.875 | 0.001 | 128.934 | + +PPE steps move the cost from `inclusion_s` (waiting for the next sealed block) to `submit_s` (the wallet itself proving the PPE circuit before sending). Each PPE prove is ≈ 127 s on this CPU. + +## Block + tx sizes (borsh) — dev mode + +Per scenario, every produced block is fetched via `getBlock(BlockId)` and serialized with `borsh::to_vec(&Block)`. Each transaction is serialized individually and counted by variant. The empty clock-only ticks at `min` give the per-block fixed-cost baseline (≈ 334 bytes across all scenarios). + +| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | +|---|---:|---:|---|---:|---:| +| token_onboarding | 6 | 881 | 334..2,890 | 206 / 8 | 2,556 / 1 | +| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | +| multi_recipient_fanout | 22 | 513 | 334..707 | 221 / 33 | n/a | +| private_chained_flow | 10 | 1,186 | 334..3,565 | 173 / 11 | 2,715 / 3 | +| parallel_fanout | 24 | 646 | 334..3,904 | 248 / 45 | n/a | + +## Block + tx sizes (borsh) — real proving + +| Scenario | blocks | block_bytes (mean) | block_bytes (min..max) | public_tx (mean / n) | ppe_tx (mean / n) | +|---|---:|---:|---|---:|---:| +| amm_swap_flow | 16 | 553 | 334..1,011 | 248 / 24 | n/a | +| private_chained_flow | 39 | 17,707 | 334..226,578 | 158 / 40 | 225,728 / 3 | + +`amm_swap_flow` is byte-identical between dev and real (no proof payload). `private_chained_flow`'s `ppe_tx_bytes` matches the cycle_bench `S_agg` measurement (≈ 225 KB borsh InnerReceipt). The `block_bytes` max (226,578) is the block containing the largest PPE transaction. + +## Findings + +- Public-only scenarios converge between dev mode and real proving in both latency and byte counts. Either mode is suitable to characterize them. +- PPE transactions are ≈ 225 KB on the wire in real proving, dominated by the outer succinct proof. Dev mode emits a ≈ 2.7 KB stub that does not represent the L1 payload; fee-model storage gas inputs must come from a real-proving run. +- Per-PPE-step prove cost on this CPU is ≈ 127 s, paid on the wallet side at submit time, not on the sequencer. For a single-program chained flow the cost stacks linearly. +- Empty clock-only ticks set the per-block fixed-cost baseline at ≈ 334 bytes across all scenarios and both modes. +- Bedrock L1 finality varies in the 6 to 32 s range across scenarios, driven by L1 cadence and which tick the closing wait happens to land on, not by the LEZ prover. + ## Reproduce Prerequisite: a running local Docker daemon (the `bedrock/docker-compose.yml` is brought up by the bench). From 33b20bb480ff20a93fb2dfbdf773dd1bfa4a5d8d Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 20 May 2026 13:08:48 +0200 Subject: [PATCH 10/11] ci(integration_bench): apply nightly rustfmt and drop integration_tests unused deps --- Cargo.lock | 7 ------- integration_tests/Cargo.toml | 7 ------- tools/integration_bench/src/harness.rs | 5 ++++- tools/integration_bench/src/scenarios/amm.rs | 20 ++++++++++++++++++-- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7efb7761..109676ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3994,29 +3994,22 @@ dependencies = [ "authenticated_transfer_core", "bytesize", "common", - "env_logger", "faucet_core", - "futures", "hex", "indexer_ffi", - "indexer_service", "indexer_service_protocol", "indexer_service_rpc", - "jsonrpsee", "key_protocol", "log", "nssa", "nssa_core", "sequencer_core", - "sequencer_service", "sequencer_service_rpc", "serde_json", "tempfile", "test_fixtures", - "testcontainers", "token_core", "tokio", - "url", "vault_core", "wallet", "wallet-ffi", diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 04cd8f8c..82d8ebd1 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -14,11 +14,9 @@ nssa_core = { workspace = true, features = ["host"] } nssa.workspace = true authenticated_transfer_core.workspace = true sequencer_core = { workspace = true, features = ["default", "testnet"] } -sequencer_service.workspace = true wallet.workspace = true common.workspace = true key_protocol.workspace = true -indexer_service.workspace = true serde_json.workspace = true token_core.workspace = true ata_core.workspace = true @@ -26,18 +24,13 @@ vault_core.workspace = true faucet_core.workspace = true indexer_service_rpc = { workspace = true, features = ["client"] } sequencer_service_rpc = { workspace = true, features = ["client"] } -jsonrpsee = { workspace = true, features = ["ws-client"] } wallet-ffi.workspace = true indexer_ffi.workspace = true indexer_service_protocol.workspace = true -url.workspace = true anyhow.workspace = true -env_logger.workspace = true log.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } hex.workspace = true tempfile.workspace = true bytesize.workspace = true -futures.workspace = true -testcontainers = { version = "0.27.3", features = ["docker-compose"] } diff --git a/tools/integration_bench/src/harness.rs b/tools/integration_bench/src/harness.rs index 9337f3ed..fb9d4d5c 100644 --- a/tools/integration_bench/src/harness.rs +++ b/tools/integration_bench/src/harness.rs @@ -61,7 +61,10 @@ pub struct ScenarioOutput { /// reporting the sequencer tip as L1-finalised. Effectively measures the /// sequencer→Bedrock posting + Bedrock finalisation + indexer L1 ingest path. /// A value at the timeout (60s) means finalisation did not happen within the bench window. - #[serde(serialize_with = "ser_opt_duration_secs", rename = "bedrock_finality_s")] + #[serde( + serialize_with = "ser_opt_duration_secs", + rename = "bedrock_finality_s" + )] pub bedrock_finality: Option, } diff --git a/tools/integration_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs index f05eed1a..79100c42 100644 --- a/tools/integration_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -24,10 +24,26 @@ pub async fn run(ctx: &mut TestContext) -> Result { let user_lp = new_public_account(ctx, &mut output, "create_acc_user_lp").await?; timed_token_new(ctx, &mut output, "token_a_new", def_a, supply_a, "TokA").await?; - timed_token_send(ctx, &mut output, "token_a_fund_user", supply_a, user_a, 1_000).await?; + timed_token_send( + ctx, + &mut output, + "token_a_fund_user", + supply_a, + user_a, + 1_000, + ) + .await?; timed_token_new(ctx, &mut output, "token_b_new", def_b, supply_b, "TokB").await?; - timed_token_send(ctx, &mut output, "token_b_fund_user", supply_b, user_b, 1_000).await?; + timed_token_send( + ctx, + &mut output, + "token_b_fund_user", + supply_b, + user_b, + 1_000, + ) + .await?; output .step(ctx, "amm_new_pool", async |ctx| { From 715d52f605e25e4dc1ed774b70ef272c8800837a Mon Sep 17 00:00:00 2001 From: Moudy Date: Wed, 20 May 2026 16:04:53 +0200 Subject: [PATCH 11/11] chore(workspace): drop integration_tests workspace dep and clean test_fixtures docstring --- Cargo.toml | 1 - test_fixtures/src/lib.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ce67c92a..b3dd2f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,6 @@ faucet_core = { path = "programs/faucet/core" } vault_core = { path = "programs/vault/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "testnet_initial_state" } -integration_tests = { path = "integration_tests" } test_fixtures = { path = "test_fixtures" } tokio = { version = "1.50", features = [ diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index da2b7be2..2c9dfb3a 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -1,9 +1,5 @@ //! Shared test/bench fixtures: spins up bedrock + sequencer + indexer + wallet //! end-to-end against docker-compose, exposes a `TestContext` callers can drive. -//! -//! Originally lived under `integration_tests`; split out so non-test consumers -//! (e.g. `integration_bench`) can depend on the fixtures without pulling in the -//! `integration_tests` test files. use std::{net::SocketAddr, path::Path, sync::LazyLock};