feat(cli): add offline block-building benchmark sub-command - #593
feat(cli): add offline block-building benchmark sub-command#593pablodeymo wants to merge 1 commit into
Conversation
`ethlambda benchmark synthetic` measures block building exactly as executed when the node proposes, against a reproducible synthetic workload, with no devnet required. "Optimize block building" (#465) is the top roadmap item, but the only observability today is Prometheus histograms on a live devnet: noisy, not reproducible, and unable to compare an optimization against a baseline. The harness drives the production proposer entry point (`produce_block_with_signatures`) over a seeded in-memory chain, seeding the pending pool per slot and letting the proposal tick promote it, as on a live node. Phases come from the existing `lean_block_proposal_attestation_build_phase_seconds` histogram: the per-label sample sums are deltaed between iterations, so attribution is exact and the hot path is untouched. Each iteration records its block root, so a baseline-vs-optimized diff proves an optimization changed only speed and not attestation selection. This is milestone M1 (mock crypto) of docs/plans/block-building-benchmark.md; M2 adds real XMSS/leanVM pools and the seal phase, M3 replay-from-datadir. Dispatch goes through the `benchmark` token that command.rs already strips, parsed by a `clap::Parser` of its own, so the harness arguments never enter CliOptions and the node parser keeps the exact shape it has today. That replaces the earlier `subcommand_negates_reqs` approach, which forced all seven node-required arguments to `Option<T>` and an unwrap helper on the node path for an invariant clap already enforced. `main` is now synchronous: it dispatches, and only the node path enters the tokio runtime — the benchmark is synchronous CPU-bound work and would otherwise park a worker thread for its whole run. Benchmark logs go to stderr at WARN so the report owns stdout and stays pipe-clean for `--format json | jq`. NEW_PAYLOAD_CAP becomes public so the harness can reject a --proofs-per-data batch the pending pool would evict whole. build.rs embeds the resolved leansig and leanVM revisions from Cargo.lock into reports, since both move the measured crypto. `make bench` runs it, and CI adds a seconds-fast mock smoke step asserting the JSON contract.
🤖 Kimi Code ReviewOverall Assessment: Well-structured, secure benchmark harness with proper isolation between node and benchmark code paths. No critical consensus bugs or security vulnerabilities. Minor maintainability issues noted below. build.rs (bin/ethlambda)Issue: Fragile manual TOML parsing of
Recommendation: Use the benchmark/corpus.rsIssue: Hardcoded pubkey size may drift from type definition.
Recommendation: Use benchmark/mod.rsIssue: Hardcoded metric name coupling.
Recommendation: Export the metric name constant from Issue: Potential silent precision loss in timing.
Recommendation: Log a warning if benchmark/report.rsIssue: Population vs. sample standard deviation.
Note: Document that CV uses population stddev if this is intentional. command.rsCode Quality: Excellent backward compatibility handling. The main.rsSecurity/Isolation: Good separation of concerns.
Consensus & State Transition CorrectnessValidations:
Note on Security
CI Integration (ci.yml)Line 78-84: The smoke test validates JSON schema version and sample count. Consider adding a determinism check: # Run twice with same seed and compare block roots
cargo run ... --format json > run1.json
cargo run ... --format json > run2.json
jq -e '.samples[].block_root' run
---
*Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt* |
🤖 Codex Code Review
Aside from those benchmark/reporting issues, I did not see a new consensus-rule regression in fork choice, attestation validation, STF, XMSS, or SSZ handling from this PR’s direct code changes. I could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
|
Split into three smaller PRs, each with a goal of its own:
#595 + #596 reconstruct what was reviewed here, plus a per-iteration table (#595 would otherwise collect the phase breakdown without ever showing it). Each of the three was verified independently: Closing in favour of that stack. |
🗒️ Description / Motivation
Adds
ethlambda benchmark synthetic— an offline harness that measures block buildingexactly as executed when the node proposes, against a reproducible synthetic
workload, with no devnet required.
"Optimize block building" (#465) is the top roadmap item, but the only observability
today is Prometheus histograms on a live devnet: noisy, not reproducible, and unable to
compare an optimization against a baseline. This is milestone M1 (mock-crypto mode) of
the design in
docs/plans/block-building-benchmark.md; M2 adds real XMSS/leanVM poolsand post-build seal-phase measurement, M3 adds replay-from-datadir.
What Changed
bin/ethlambda/src/benchmark/{mod,corpus,report}.rsInMemoryBackend, per-slot pool seeding into the pending pool (promoted by the proposal tick, as in production), iteration loop drivingproduce_block_with_signatures+ block import; stats + human/JSON reportbin/ethlambda/src/command.rsbenchmarkjoinsnodeas a dispatched token, parsed by aclap::Parserof its own so harness arguments never enterCliOptions; argv[0] is rewritten toethlambda benchmarkso usage lines name the sub-command that owns thembin/ethlambda/src/main.rsmainis synchronous: it dispatches, and only the node path enters the tokio runtime (run_nodecarries the#[tokio::main]attributes). Benchmark logs go to stderr at WARN so the report owns stdoutcrates/storage/{lib,store}.rsNEW_PAYLOAD_CAPso the harness rejects--proofs-per-databatches the pending pool would evict wholebin/ethlambda/build.rsCargo.lockinto reports (leansig tracks the movingdevnet4branch; leanVM does the aggregation, so its pinned rev moves the measured crypto too). The per-[[package]]parse collectsnameandsourcebefore extracting the rev, so it does not depend on TOML field orderMakefile,.github/workflows/ci.ymlmake bench; seconds-fast mock smoke step in the Test job validating the JSON contractdocs/plans/block-building-benchmark.mdCorrectness / Behavior Guarantees
cli.rsis not touched, and the node runtime is unchanged. The seven node-requiredarguments stay plain
PathBuf/String, so clap keeps emitting its ownmissing-argument errors and there is no
Option<T>to unwrap on the node path. Thatreplaces feat(cli): add offline block-building benchmark subcommand #497's
subcommand_negates_reqsapproach, which needed both.the JSON, so a baseline-vs-optimized diff proves an optimization changed only speed,
not attestation selection). Verified across repeated runs. The harness never reads the
wall clock into results.
select_payloads/compact/stf_simulatecome from the sample sums of the existinglean_block_proposal_attestation_build_phase_secondshistogram, deltaed betweeniterations;
overheadis the clamped remainder of wall minus the phases.duration of a CPU-bound run.
Tests Added / Run
command.rsunit tests cover the benchmark token: it parses with no node argument, itrejects node flags, and its usage line names the sub-command.
make bench; the CI smoke assertion(
jq -e '.schema_version == 1 and (.samples | length == 3)'); identical block-rootsequences across two runs at the same seed; and
ethlambda --genesis config.yamlstillfailing with clap's own missing-argument list.
make fmt,make lint,make test(582 tests, 30 suites) — all clean.Related Issues / PRs
nodesub-command for running the node #591✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — all passing