From 9a61422668b7aba180ebbd3d140ed49c7742f70c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 24 Jun 2026 17:39:09 -0300 Subject: [PATCH] feat(bench): ethrex distinct-transfer benchmark baseline + memory sweep Replace the PR benchmark's headline program (fib_iterative_8M) with the ethrex guest proving a 20-transfer block, and the memory-growth sweep (fib 1M..8M) with an ethrex transfer-count sweep (4/8/12/16/20). Transfers use N distinct, genesis-funded senders -> N distinct recipients ("distinct" mode), so the state-trie witness reflects a realistic block rather than repeated same-account transfers. - tooling/ethrex-fixtures: add optional `mode` arg (same|recipients|distinct). distinct injects deterministic synthetic senders into the genesis allocation and uses a per-index tip so block ordering (and output bytes) are reproducible. - benchmark-pr.yml: build the ethrex ELF + generate fixtures in-job (gitignored, not committed); prove with --private-input. Growth runs at default parallelism, 1 sample/point (run-to-run heap variance ~0). /bench-growth no longer forces k=1. - executor/.gitignore: ignore the generated bench fixtures. --- .github/workflows/benchmark-pr.yml | 138 +++++++++++++++------------- executor/.gitignore | 1 + tooling/ethrex-fixtures/README.md | 23 +++-- tooling/ethrex-fixtures/src/main.rs | 116 +++++++++++++++++++++-- 4 files changed, 199 insertions(+), 79 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index c0f8b2670..ca66bf9a7 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -11,6 +11,7 @@ on: - 'crypto/**' - 'executor/**' - 'bin/cli/**' + - 'tooling/ethrex-fixtures/**' # Uncomment to auto-run on PRs: # pull_request: # branches: [main] @@ -30,12 +31,19 @@ concurrency: cancel-in-progress: true env: - PROGRAM: executor/programs/asm/fib_iterative_8M.s - ELF: executor/program_artifacts/asm/fib_iterative_8M.elf + # Headline program: the ethrex guest ELF proven against a 20-transfer block + # (distinct sender -> distinct recipient per tx). One ELF; the workload is the + # private input (rkyv ProgramInput), generated in-job and gitignored (see the + # "Generate ethrex bench fixtures" step). + ELF: executor/program_artifacts/rust/ethrex.elf + INPUT: executor/tests/ethrex_bench_20.bin BENCH_RUNS_PR: 3 BENCH_RUNS_BASELINE: 3 - GROWTH_PROGRAMS: "fib_iterative_1M fib_iterative_2M fib_iterative_4M fib_iterative_8M" - GROWTH_STEPS: "1000000 2000000 4000000 8000000" + # Memory-scaling sweep: same ELF, different N-transfer inputs. GROWTH_PROGRAMS + # are the generated (gitignored) fixture basenames in executor/tests/; GROWTH_STEPS + # the matching transfer counts (x-axis; slope is MB per transfer). + GROWTH_PROGRAMS: "ethrex_bench_4 ethrex_bench_8 ethrex_bench_12 ethrex_bench_16 ethrex_bench_20" + GROWTH_STEPS: "4 8 12 16 20" jobs: benchmark: @@ -76,28 +84,30 @@ jobs: with: ref: ${{ steps.pr-ref.outputs.sha || github.sha }} - - name: Compile benchmark ELFs - run: | - mkdir -p executor/program_artifacts/asm - # Compile main benchmark ELF - MAIN_SRC="$PROGRAM" - MAIN_OUT="$ELF" - if [ ! -f "$MAIN_OUT" ] && [ -f "$MAIN_SRC" ]; then - clang --target=riscv64 -march=rv64im -fuse-ld=lld -nostdlib -Wl,-e,main \ - "$MAIN_SRC" -o "$MAIN_OUT" - fi - for prog in $GROWTH_PROGRAMS; do - SRC="executor/programs/asm/${prog}.s" - OUT="executor/program_artifacts/asm/${prog}.elf" - if [ ! -f "$OUT" ]; then - clang --target=riscv64 -march=rv64im -fuse-ld=lld -nostdlib -Wl,-e,main \ - "$SRC" -o "$OUT" - fi - done - - name: Add cargo to PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Build ethrex guest ELF + run: | + # Self-provision the RV64 sysroot in a user-writable dir (matches the + # nightly bench job); make picks it up via SYSROOT_DIR ?= and passes it + # to clang as --sysroot. The ELF is gitignored and persists across the + # baseline `git checkout`, so the same workload is proven on both sides. + export SYSROOT_DIR="$HOME/.lambda-vm-sysroot" + make executor/program_artifacts/rust/ethrex.elf + + - name: Generate ethrex bench fixtures + run: | + # Generated, not committed (gitignored via executor/.gitignore). They are + # untracked, so they survive the baseline `git checkout origin/main` below — + # the SAME workload (ELF + inputs) is proven on both the PR and main sides. + # distinct = N independent genesis-funded senders -> N distinct recipients. + ( cd tooling/ethrex-fixtures && cargo build --release ) + GEN=tooling/ethrex-fixtures/target/release/ethrex-fixtures + for n in $GROWTH_STEPS; do + "$GEN" "$n" "executor/tests/ethrex_bench_${n}.bin" distinct + done + - name: Build CLI (PR) run: cargo build --release -p cli --features jemalloc-stats @@ -134,13 +144,11 @@ jobs: echo "runs=$RUNS" >> "$GITHUB_OUTPUT" - # Parse TABLE_PARALLELISM: - # /bench-growth always uses k=1 (for reproducible comparisons) - # /bench accepts k=N parameter + # Optional table parallelism for the HEADLINE benchmark only (the memory + # growth sweep always runs at default parallelism). `/bench k=N` overrides; + # otherwise default (cores/3). /bench-growth no longer forces k=1. TABLE_K="" - if [ "$EVENT_NAME" = "issue_comment" ] && echo "$COMMENT_BODY" | grep -q '^/bench-growth'; then - TABLE_K="1" - elif [ "$EVENT_NAME" = "issue_comment" ]; then + if [ "$EVENT_NAME" = "issue_comment" ]; then TABLE_K=$(echo "$COMMENT_BODY" | grep -o 'k=[0-9]*' | head -1 | cut -d= -f2) fi echo "table_parallelism=${TABLE_K:-}" >> "$GITHUB_OUTPUT" @@ -166,7 +174,7 @@ jobs: HEAPS="" for i in $(seq 1 $RUNS); do echo "--- Run $i/$RUNS ---" - ./target/release/cli prove "$ELF" -o /tmp/proof.bin --time \ + ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \ | tee /tmp/cli_output_$i.txt rm -f /tmp/proof.bin @@ -224,23 +232,23 @@ jobs: - name: Memory growth (PR) id: pr-growth if: steps.config.outputs.run_growth == 'true' - env: - TABLE_PARALLELISM: "1" run: | PROGRAMS=($GROWTH_PROGRAMS) STEPS_ARR=($GROWTH_STEPS) GROWTH_HEAPS="" GROWTH_TIMES="" - SAMPLES=2 + # 1 sample/point: run-to-run heap is ~deterministic (<0.3%), so an extra + # transfer-count point buys more slope accuracy than a replicate. + SAMPLES=1 for idx in "${!PROGRAMS[@]}"; do prog="${PROGRAMS[$idx]}" - ELF_PATH="executor/program_artifacts/asm/${prog}.elf" + INPUT_PATH="executor/tests/${prog}.bin" SAMPLE_HEAPS="" SAMPLE_TIMES="" for s in $(seq 1 $SAMPLES); do - echo "--- Growth: $prog (sample $s/$SAMPLES, TABLE_PARALLELISM=1) ---" - ./target/release/cli prove "$ELF_PATH" -o /tmp/proof.bin --time \ + echo "--- Growth: $prog (sample $s/$SAMPLES, default parallelism) ---" + ./target/release/cli prove "$ELF" --private-input "$INPUT_PATH" -o /tmp/proof.bin --time \ | tee /tmp/growth_${prog}_${s}.txt rm -f /tmp/proof.bin T=$(grep -o 'Proving time: [0-9.]*' /tmp/growth_${prog}_${s}.txt | awk '{print $3}') @@ -258,14 +266,14 @@ jobs: GROWTH_TIMES="${GROWTH_TIMES:+$GROWTH_TIMES/}$T" done - # Linear regression: heap (MB) vs steps (millions) + # Linear regression: heap (MB) vs transfer count (slope = MB per transfer) STEPS_SLASH=$(echo "${STEPS_ARR[@]}" | tr ' ' '/') read SLOPE R2 <<< $(awk -v steps="$STEPS_SLASH" -v heaps="$GROWTH_HEAPS" 'BEGIN { n = split(steps, xs, "/") split(heaps, ys, "/") sx = 0; sy = 0; sxy = 0; sx2 = 0 for (i = 1; i <= n; i++) { - x = xs[i] / 1000000; y = ys[i] + 0 + x = xs[i]; y = ys[i] + 0 sx += x; sy += y; sxy += x * y; sx2 += x * x } d = n * sx2 - sx * sx @@ -273,7 +281,7 @@ jobs: slope = (n * sxy - sx * sy) / d my = sy / n; ss_tot = 0; ss_res = 0 for (i = 1; i <= n; i++) { - x = xs[i] / 1000000; y = ys[i] + 0 + x = xs[i]; y = ys[i] + 0 pred = slope * x + (sy - slope * sx) / n ss_res += (y - pred) * (y - pred) ss_tot += (y - my) * (y - my) @@ -355,18 +363,20 @@ jobs: # Save current HEAD PR_SHA=$(git rev-parse HEAD) - # Checkout main and build + # Checkout main and rebuild the prover (CLI) only. The workload — the gitignored + # ethrex ELF and the generated, untracked bench fixtures — is left untouched by + # the checkout, so the same inputs are proven on both the PR and main sides. git fetch origin main git checkout origin/main cargo build --release -p cli --features jemalloc-stats - # --- Primary benchmark (2M) --- + # --- Primary benchmark (ethrex 20 transfers) --- TIMES="" HEAPS="" for i in $(seq 1 $RUNS); do echo "--- Baseline run $i/$RUNS ---" - ./target/release/cli prove "$ELF" -o /tmp/proof.bin --time \ + ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \ | tee /tmp/baseline_output_$i.txt rm -f /tmp/proof.bin @@ -411,7 +421,7 @@ jobs: echo "all_heaps=$ALL_HEAPS" >> "$GITHUB_OUTPUT" echo "runs=$RUNS" >> "$GITHUB_OUTPUT" - # --- Growth benchmarks (TABLE_PARALLELISM=1, 2 samples each) --- + # --- Growth benchmarks (default parallelism, 1 sample each) --- # Only run if /bench-growth, push, or workflow_dispatch if [ "$RUN_GROWTH" != "true" ]; then echo "Skipping growth benchmarks (use /bench-growth to enable)" @@ -420,16 +430,16 @@ jobs: STEPS_ARR=($GROWTH_STEPS) GROWTH_HEAPS="" GROWTH_TIMES="" - SAMPLES=2 + SAMPLES=1 for idx in "${!PROGRAMS[@]}"; do prog="${PROGRAMS[$idx]}" - ELF_PATH="executor/program_artifacts/asm/${prog}.elf" + INPUT_PATH="executor/tests/${prog}.bin" SAMPLE_HEAPS="" SAMPLE_TIMES="" for s in $(seq 1 $SAMPLES); do - echo "--- Baseline growth: $prog (sample $s/$SAMPLES, TABLE_PARALLELISM=1) ---" - TABLE_PARALLELISM=1 ./target/release/cli prove "$ELF_PATH" -o /tmp/proof.bin --time \ + echo "--- Baseline growth: $prog (sample $s/$SAMPLES, default parallelism) ---" + ./target/release/cli prove "$ELF" --private-input "$INPUT_PATH" -o /tmp/proof.bin --time \ | tee /tmp/baseline_growth_${prog}_${s}.txt rm -f /tmp/proof.bin T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_growth_${prog}_${s}.txt | awk '{print $3}') @@ -453,7 +463,7 @@ jobs: split(heaps, ys, "/") sx = 0; sy = 0; sxy = 0; sx2 = 0 for (i = 1; i <= n; i++) { - x = xs[i] / 1000000; y = ys[i] + 0 + x = xs[i]; y = ys[i] + 0 sx += x; sy += y; sxy += x * y; sx2 += x * x } d = n * sx2 - sx * sx @@ -461,7 +471,7 @@ jobs: slope = (n * sxy - sx * sy) / d my = sy / n; ss_tot = 0; ss_res = 0 for (i = 1; i <= n; i++) { - x = xs[i] / 1000000; y = ys[i] + 0 + x = xs[i]; y = ys[i] + 0 pred = slope * x + (sy - slope * sx) / n ss_res += (y - pred) * (y - pred) ss_tot += (y - my) * (y - my) @@ -675,7 +685,7 @@ jobs: const nLabel = parseInt(runs) > 1 ? ` (median of ${runs})` : ''; const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; - let body = `## Benchmark — fib_iterative_8M${nLabel}\n\n`; + let body = `## Benchmark — ethrex 20 transfers${nLabel}\n\n`; body += `Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Δ |\n`; body += `|--------|------|----|---|\n`; @@ -730,34 +740,35 @@ jobs: if (prGrowthHeaps) { const prHeaps = prGrowthHeaps.split('/'); const baseHeaps = baseGrowthHeaps ? baseGrowthHeaps.split('/') : null; - const labels = ['1M', '2M', '4M', '8M']; - const programs = ['fib_iterative_1M', 'fib_iterative_2M', 'fib_iterative_4M', 'fib_iterative_8M']; + // Transfer counts (x-axis); keep in sync with GROWTH_STEPS in the env block. + const labels = ['4', '8', '12', '16', '20']; + const n = prHeaps.length; body += `\n## Memory Growth\n\n`; - body += `Measured with \`TABLE_PARALLELISM=1\` (sequential) · best of 2 samples per point\n\n`; + body += `ethrex distinct-account transfers · default parallelism · 1 sample per point\n\n`; - if (baseHeaps && baseHeaps.length === 4 && baseHeaps[0]) { - body += `| Program | Steps | main (MB) | PR (MB) | Δ |\n`; - body += `|---------|-------|-----------|---------|---|\n`; - for (let i = 0; i < 4; i++) { + if (baseHeaps && baseHeaps.length === n && baseHeaps[0]) { + body += `| Transfers | main (MB) | PR (MB) | Δ |\n`; + body += `|-----------|-----------|---------|---|\n`; + for (let i = 0; i < n; i++) { const bh = parseInt(baseHeaps[i]); const ph = parseInt(prHeaps[i]); const diff = ph - bh; const pct = bh > 0 ? ((diff / bh) * 100).toFixed(1) : '0.0'; - body += `| ${programs[i]} | ${labels[i]} | ${baseHeaps[i]} | ${prHeaps[i]} | ${fmt(diff)} MB (${fmt(pct)}%) |\n`; + body += `| ${labels[i]} | ${baseHeaps[i]} | ${prHeaps[i]} | ${fmt(diff)} MB (${fmt(pct)}%) |\n`; } } else { - body += `| Program | Steps | PR (MB) |\n`; - body += `|---------|-------|---------|\n`; - for (let i = 0; i < 4; i++) { - body += `| ${programs[i]} | ${labels[i]} | ${prHeaps[i]} |\n`; + body += `| Transfers | PR (MB) |\n`; + body += `|-----------|---------|\n`; + for (let i = 0; i < n; i++) { + body += `| ${labels[i]} | ${prHeaps[i]} |\n`; } } body += `\n`; if (prGrowthSlope) { - body += `**Growth rate:** ${prGrowthSlope} MB / 1M steps`; + body += `**Growth rate:** ${prGrowthSlope} MB / transfer`; if (baseGrowthSlope && growthSlopePct) { body += ` (main: ${baseGrowthSlope}, Δ: ${fmt(growthSlopePct)}%)`; } @@ -796,6 +807,7 @@ jobs: // Find existing comment (check both old and new markers for transition) const existing = comments.find(c => c.user.type === 'Bot' && ( + c.body.includes('Benchmark — ethrex') || c.body.includes('Benchmark — fib_iterative_8M') || c.body.includes('Benchmark — fib_iterative_2M') || c.body.includes('Benchmark — fib_iterative_372k') diff --git a/executor/.gitignore b/executor/.gitignore index 55aaf98cf..fa48867ab 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -1,3 +1,4 @@ /target /program_artifacts/rust /tests/ethrex_hoodi.bin +/tests/ethrex_bench_*.bin diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index b93194504..7c9b00e0f 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -19,19 +19,28 @@ this crate's `Cargo.toml` too and regenerate. ```bash cd tooling/ethrex-fixtures -cargo run --release -- +cargo run --release -- [mode] ``` - `` — how many ETH transfers to include in the block (`0` = empty block). - `` — where to write the `.bin` (relative to this directory). +- `[mode]` — account diversity (optional, default `same`): + - `same` — one funded sender (`RICH_PK`) → one fixed recipient (`0xdeadbeef`). + - `recipients` — one funded sender → N distinct recipients (1 → N fan-out). + - `distinct` — N distinct, genesis-funded senders → N distinct recipients + (N independent 1-1 pairs; senders are deterministic synthetic keys injected + into the genesis allocation). This is what the CI benchmark uses, since the + state-trie witness for many distinct accounts is closer to a real block. -It prints the output size and the number of transactions included, e.g.: +It prints the output size, the number of transactions, and the mode, e.g.: ``` -wrote ../../executor/tests/ethrex_simple_tx.bin (12745 bytes): block #1 with 1/1 transfer(s) +wrote ../../executor/tests/ethrex_simple_tx.bin (12745 bytes): block #1 with 1/1 transfer(s) [1 sender -> 1 recipient] ``` +Output is deterministic for a given `(n_transfers, mode)`. + ## Creating blocks with different numbers of transactions Just change the first argument: @@ -59,9 +68,11 @@ it regenerates the standard fixtures and refreshes > machine — e.g. 10 transfers ≈ 42M cycles. ## Details -- Transactions are plain ETH transfers signed by a funded dev account from - `genesis.json` (well-known load-test key — not a secret), so output is - deterministic. +- Transactions are plain ETH transfers. In `same`/`recipients` mode they are + signed by a funded dev account from `genesis.json` (well-known load-test key — + not a secret); in `distinct` mode each is signed by its own synthetic key, + funded by injecting an entry into the genesis allocation. Output is + deterministic in all modes. - Currently only ETH transfers are supported. (ERC20 / contract calls would be a future extension.) - Once the upstream LambdaVM-backend ethrex PR merges, this tool can be replaced diff --git a/tooling/ethrex-fixtures/src/main.rs b/tooling/ethrex-fixtures/src/main.rs index 4ab58d21b..f4a55bbc0 100644 --- a/tooling/ethrex-fixtures/src/main.rs +++ b/tooling/ethrex-fixtures/src/main.rs @@ -2,10 +2,17 @@ //! lambda-vm prover/benchmarks — in-memory, offline, deterministic. //! //! Usage: -//! cargo run -- +//! cargo run -- [mode] +//! +//! mode (optional, default `same`): +//! same all txs: the rich sender -> one fixed recipient (0xdeadbeef) +//! recipients the rich sender -> N distinct recipients (1 -> N fan-out) +//! distinct N distinct, genesis-funded senders -> N distinct recipients +//! (N independent, unrelated 1-1 account pairs) //! e.g. //! cargo run -- 1 ../../executor/tests/ethrex_simple_tx.bin //! cargo run -- 10 ../../executor/tests/ethrex_10_transfers.bin +//! cargo run -- 20 /tmp/ethrex_20_distinct.bin distinct //! //! TODO(ethrex-integration, PR #666): TEMPORARY. Delete this whole crate once //! the LambdaVM-backend ethrex PR lands on ethrex `main` and fixtures are @@ -18,7 +25,7 @@ use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; use ethrex_blockchain::{Blockchain, BlockchainOptions}; use ethrex_common::types::{ - EIP1559Transaction, ELASTICITY_MULTIPLIER, Genesis, Transaction, TxKind, + EIP1559Transaction, ELASTICITY_MULTIPLIER, Genesis, GenesisAccount, Transaction, TxKind, }; use ethrex_common::{Address, H256, U256}; use ethrex_guest_program::l1::ProgramInput; @@ -31,11 +38,49 @@ use secp256k1::SecretKey; const RICH_PK: &str = "bcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31"; const GENESIS_JSON: &str = include_str!("../genesis.json"); +/// How the block's transactions distribute across accounts. +#[derive(Clone, Copy, PartialEq)] +enum Mode { + /// All txs: the rich sender -> one fixed recipient (original behavior). + Same, + /// The rich sender -> N distinct recipients (1 -> N fan-out). + Recipients, + /// N distinct, genesis-funded senders -> N distinct recipients. + Distinct, +} + +fn parse_mode(s: &str) -> Option { + match s { + "same" => Some(Mode::Same), + "recipients" | "fanout" => Some(Mode::Recipients), + "distinct" | "diverse" => Some(Mode::Distinct), + _ => None, + } +} + fn usage_and_exit(program: &str) -> ! { - eprintln!("usage: {program} "); + eprintln!("usage: {program} [same|recipients|distinct]"); std::process::exit(2); } +/// Deterministic, distinct, valid secp256k1 signer for sender index `i`. +/// Key = 0x01 ‖ 0…0 ‖ big-endian(i): always nonzero and far below the curve order. +fn deterministic_signer(i: u64) -> Signer { + let mut sk = [0u8; 32]; + sk[0] = 1; + sk[24..32].copy_from_slice(&i.to_be_bytes()); + LocalSigner::new(SecretKey::from_slice(&sk).expect("valid secret key")).into() +} + +fn rich_signer() -> Result> { + Ok(LocalSigner::new(SecretKey::from_slice(&hex::decode(RICH_PK)?)?).into()) +} + +/// Distinct recipient address for tx index `i` (fresh account, no funding needed). +fn recipient_for(i: u64) -> Address { + Address::from_low_u64_be(0xdead_0000_0000u64 + i) +} + #[tokio::main] async fn main() -> Result<(), Box> { let mut args = std::env::args(); @@ -46,6 +91,13 @@ async fn main() -> Result<(), Box> { let Some(out_path) = args.next() else { usage_and_exit(&program); }; + let mode = match args.next() { + None => Mode::Same, + Some(s) => match parse_mode(&s) { + Some(m) => m, + None => usage_and_exit(&program), + }, + }; if args.next().is_some() { usage_and_exit(&program); } @@ -54,7 +106,23 @@ async fn main() -> Result<(), Box> { }; // --- 1. genesis -> in-memory store ------------------------------------- - let genesis: Genesis = serde_json::from_str(GENESIS_JSON)?; + let mut genesis: Genesis = serde_json::from_str(GENESIS_JSON)?; + + // For `distinct`, fund each synthetic sender in genesis so its tx is valid. + if mode == Mode::Distinct { + for i in 0..n_transfers { + genesis.alloc.insert( + deterministic_signer(i).address(), + GenesisAccount { + code: Bytes::new(), + storage: Default::default(), + balance: U256::from(100_000_000_000_000_000_000u128), // 100 ETH + nonce: 0, + }, + ); + } + } + let chain_id = genesis.config.chain_id; let mut store = Store::new(".ethrex-fixtures-tmp", EngineType::InMemory)?; store.add_initial_state(genesis).await?; @@ -69,13 +137,36 @@ async fn main() -> Result<(), Box> { let blockchain = Blockchain::new(store.clone(), BlockchainOptions::default()); // --- 2. build + sign N transfers, push to the mempool ------------------ - let signer: Signer = LocalSigner::new(SecretKey::from_slice(&hex::decode(RICH_PK)?)?).into(); - let recipient = Address::from_low_u64_be(0xdead_beef); - for nonce in 0..n_transfers { + // `same`: rich sender, nonce 0..N, fixed recipient. + // `recipients`: rich sender, nonce 0..N, distinct recipient per tx. + // `distinct`: distinct sender per tx (nonce 0), distinct recipient per tx. + for i in 0..n_transfers { + // `distinct` senders all use nonce 0 with otherwise-identical fees, so the + // payload builder would tie-break block order by the mempool's wall-clock + // insertion time (and hash-map iteration order) — nondeterministic. A unique + // per-index tip makes the order a strict function of `i` (tip descending), + // so the output bytes are reproducible regardless of timing/platform. `same` + // and `recipients` keep the constant tip (single sender, nonce-ordered), so + // the committed same-mode fixtures' checksums are unaffected. + let (signer, nonce, recipient, priority_fee) = match mode { + Mode::Same => ( + rich_signer()?, + i, + Address::from_low_u64_be(0xdead_beef), + 1_000_000_000u64, + ), + Mode::Recipients => (rich_signer()?, i, recipient_for(i), 1_000_000_000u64), + Mode::Distinct => ( + deterministic_signer(i), + 0, + recipient_for(i), + 1_000_000_000u64 + i, + ), + }; let mut tx = Transaction::EIP1559Transaction(EIP1559Transaction { chain_id, nonce, - max_priority_fee_per_gas: 1_000_000_000, + max_priority_fee_per_gas: priority_fee, max_fee_per_gas: 100_000_000_000, gas_limit: 21_000, to: TxKind::Call(recipient), @@ -113,14 +204,19 @@ async fn main() -> Result<(), Box> { // --- 4. stateless witness -> ProgramInput -> rkyv ---------------------- let witness = blockchain - .generate_witness_for_blocks(&[block.clone()]) + .generate_witness_for_blocks(std::slice::from_ref(&block)) .await?; let program_input = ProgramInput::new(vec![block], witness); let bytes = rkyv::to_bytes::(&program_input)?; std::fs::write(&out_path, &bytes)?; + let mode_label = match mode { + Mode::Same => "1 sender -> 1 recipient", + Mode::Recipients => "1 sender -> N recipients", + Mode::Distinct => "N senders -> N recipients", + }; println!( - "wrote {out_path} ({} bytes): block #{} with {included}/{n_transfers} transfer(s)", + "wrote {out_path} ({} bytes): block #{} with {included}/{n_transfers} transfer(s) [{mode_label}]", bytes.len(), head_number + 1, );