From fb3216af94563da0a06f60a9348411a7d45537b7 Mon Sep 17 00:00:00 2001 From: Nicole Date: Mon, 27 Jul 2026 17:40:51 -0300 Subject: [PATCH 01/27] Add a converter for real ethrex blocks --- .github/workflows/pr_main.yaml | 19 +- Makefile | 31 +- executor/.gitignore | 4 +- executor/tests/README.md | 10 + tooling/ethrex-real-block/.gitignore | 4 + tooling/ethrex-real-block/Cargo.lock | 3345 +++++++++++++++++++++++++ tooling/ethrex-real-block/Cargo.toml | 37 + tooling/ethrex-real-block/README.md | 144 ++ tooling/ethrex-real-block/src/main.rs | 136 + tooling/ethrex-tests/tests/ethrex.rs | 50 + 10 files changed, 3776 insertions(+), 4 deletions(-) create mode 100644 tooling/ethrex-real-block/.gitignore create mode 100644 tooling/ethrex-real-block/Cargo.lock create mode 100644 tooling/ethrex-real-block/Cargo.toml create mode 100644 tooling/ethrex-real-block/README.md create mode 100644 tooling/ethrex-real-block/src/main.rs diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index a4554fda2..f6460cd2c 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -101,15 +101,32 @@ jobs: run: | cargo test --release -p executor test_ckzg -- --ignored + # The real-block fixture (~1 MB) is gitignored and generated on demand; + # test_ethrex_real_block_native below needs it present. This also + # downloads the ethrex-replay cache the converter's own tests read. + - name: Generate ethrex real-block fixture + run: make ethrex-real-block-fixture + + # Detached workspace, and the only place the block is executed against the + # guest's actual precompile surface (lambdavm features + LambdaVmEcsmCrypto), + # so it is what catches a block needing an accelerator we don't have. + - name: Run ethrex real-block converter tests (detached workspace) + run: | + cd tooling/ethrex-real-block && cargo test --release + # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature, which must not # feature-unify with the main workspace's aligned proof format), so run # them from that directory to use its isolated Cargo.lock. The guest ELF # and committed fixtures are already present from the steps above. # --include-ignored also runs the heavier synthetic-block test. + # `test_ethrex_real_block_vm` is skipped: it drives a real block through + # the VM and its runtime is not yet measured, so it stays opt-in rather + # than sitting in the PR gate. Run it manually on a build server. - name: Run ethrex host-reference tests (detached workspace) run: | - cd tooling/ethrex-tests && cargo test --release -- --include-ignored + cd tooling/ethrex-tests && \ + cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm test-cli: name: CLI tests diff --git a/Makefile b/Makefile index 4abed4a90..a95f9f49a 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ test-profile-recursion-block recursion-profile-block-input \ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \ test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ -update-ethrex-fixture-checksums check-ethrex-fixture-checksums +update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture UNAME := $(shell uname) @@ -271,9 +271,36 @@ test-asm: compile-programs-asm test-rust: compile-programs-rust cargo test -p executor --test rust +# Real-block fixture: a genuine Hoodi block (4.4M gas, 11 txs, ~124 KB of +# contract bytecode, 1705 state-trie nodes), as opposed to the synthetic +# N-plain-transfer blocks from tooling/ethrex-fixtures. ~1 MB, so it is +# generated on demand and gitignored rather than committed. +# +# The source is ethrex-replay's cache JSON, not that tool's own rkyv output: +# ethrex-replay tracks ethrex `main`, where `ProgramInput` has diverged from the +# rev our guest pins, so only the JSON is safe to read across the two. +ETHREX_REAL_BLOCK ?= 1265656 +# Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would +# let the benchmark's input change under a fixed fixture name, silently breaking +# comparability across runs. Re-pin deliberately when adopting a new block. +ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 +ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json +ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_hoodi_$(ETHREX_REAL_BLOCK).bin + +ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) + +$(ETHREX_REAL_BLOCK_CACHE): + mkdir -p $(dir $@) + curl -fsSL -o $@ \ + https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json + +$(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-block/src/main.rs + cd tooling/ethrex-real-block && \ + cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$@ + # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock). -test-ethrex: compile-programs-rust +test-ethrex: compile-programs-rust $(ETHREX_REAL_BLOCK_FIXTURE) cd tooling/ethrex-tests && cargo test --release -- --include-ignored test-flamegraph: diff --git a/executor/.gitignore b/executor/.gitignore index 17f7497d7..c03cd0a0a 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -1,6 +1,8 @@ /target /program_artifacts/rust -/tests/ethrex_hoodi.bin +# Real-block fixtures (~1 MB): generated by `make ethrex-real-block-fixture` +# from an ethrex-replay cache, never committed. See tooling/ethrex-real-block. +/tests/ethrex_hoodi*.bin /tests/ethrex_bench_*.bin # _4 is committed (~17 KB): used by `make recursion-profile-block-input` and # scripts/bench_recursion_scaling.sh. Other sizes stay ignored β€” scaling.sh diff --git a/executor/tests/README.md b/executor/tests/README.md index 39fd13813..820963ea1 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -52,3 +52,13 @@ ethrex_10_transfers.bin sha256: 38901ee4d40b99cf0aa7f642a92f0fc8db76d974bf43033a1673839020c3c28e contents: stateless ethrex block with ten plain ETH transfer transactions ``` + +## Real-block fixtures + +The blocks above are synthetic (N plain ETH transfers over a small genesis). +For a representative workload β€” real contract execution, real trie depth, real +bytecode β€” `make ethrex-real-block-fixture` generates +`ethrex_hoodi_1265656.bin` from an ethrex-replay cache. It is ~1 MB, so it is +gitignored and generated on demand rather than committed, and its checksum is +pinned in `tooling/ethrex-real-block/README.md` rather than above (the +checksum script only covers committed fixtures). diff --git a/tooling/ethrex-real-block/.gitignore b/tooling/ethrex-real-block/.gitignore new file mode 100644 index 000000000..6d50c2d01 --- /dev/null +++ b/tooling/ethrex-real-block/.gitignore @@ -0,0 +1,4 @@ +/target +# Downloaded ethrex-replay cache JSONs (~1.5 MB each), fetched on demand by +# `make ethrex-real-block-fixture`. +/caches/ diff --git a/tooling/ethrex-real-block/Cargo.lock b/tooling/ethrex-real-block/Cargo.lock new file mode 100644 index 000000000..dc1c04d3e --- /dev/null +++ b/tooling/ethrex-real-block/Cargo.lock @@ -0,0 +1,3345 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1" +dependencies = [ + "digest", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "blst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d04308254695569fdb9bfe3bacc1c91837a670d0806605eb82d63748fbd3a6" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "concat-kdf" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d72c1252426a83be2092dd5884a5f6e3b8e7180f6891b6263d2c21b92ec8816" +dependencies = [ + "digest", +] + +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +dependencies = [ + "const-default", + "critical-section", + "linked_list_allocator", + "rlsf", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "ethbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c321610643004cf908ec0f5f2aa0d8f1f8e14b540562a2887a1111ff1ecbf7b" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + +[[package]] +name = "ethrex-blockchain" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "crossbeam", + "ethrex-common", + "ethrex-crypto", + "ethrex-metrics", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "ethrex-vm", + "rayon", + "rustc-hash", + "thiserror 2.0.19", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "ethrex-common" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "crc32fast", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-trie", + "hex", + "hex-literal", + "hex-simd", + "indexmap 2.14.0", + "lazy_static", + "libc", + "lru", + "once_cell", + "rayon", + "rkyv", + "rustc-hash", + "secp256k1", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "ethrex-config" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "ethrex-common", + "ethrex-p2p", + "hex", + "serde", + "serde_json", +] + +[[package]] +name = "ethrex-crypto" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "bls12_381", + "c-kzg", + "ethereum-types", + "ff", + "hex-literal", + "k256", + "malachite", + "num-bigint", + "p256", + "ripemd", + "secp256k1", + "sha2", + "thiserror 2.0.19", + "tiny-keccak", +] + +[[package]] +name = "ethrex-guest-program" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "ethrex-l2-common", + "ethrex-rlp", + "ethrex-vm", + "hex", + "k256", + "lambda-vm-syscalls", + "rkyv", + "serde", + "serde_with", + "thiserror 2.0.19", +] + +[[package]] +name = "ethrex-l2-common" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "ethereum-types", + "ethrex-common", + "ethrex-crypto", + "k256", + "lambdaworks-crypto", + "rkyv", + "secp256k1", + "serde", + "serde_with", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "ethrex-levm" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "derive_more", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "malachite", + "rustc-hash", + "serde", + "strum", + "thiserror 2.0.19", +] + +[[package]] +name = "ethrex-metrics" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "ethrex-common", + "serde", + "serde_json", + "thiserror 2.0.19", + "tracing-subscriber", +] + +[[package]] +name = "ethrex-p2p" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "aes", + "aes-gcm", + "bytes", + "concat-kdf", + "crossbeam", + "ctr", + "ethereum-types", + "ethrex-blockchain", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-storage", + "ethrex-trie", + "futures", + "hex", + "hkdf", + "hmac", + "indexmap 2.14.0", + "lazy_static", + "lru", + "prometheus", + "rand 0.8.7", + "rayon", + "rustc-hash", + "secp256k1", + "serde", + "sha2", + "snap", + "spawned-concurrency", + "spawned-rt", + "thiserror 2.0.19", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "ethrex-real-block" +version = "0.1.0" +dependencies = [ + "ethrex-common", + "ethrex-config", + "ethrex-guest-program", + "lambda-vm-ethrex-crypto", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "ethrex-rlp" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "ethereum-types", + "thiserror 2.0.19", +] + +[[package]] +name = "ethrex-storage" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "anyhow", + "bytes", + "ethrex-common", + "ethrex-crypto", + "ethrex-rlp", + "ethrex-trie", + "fastbloom", + "lru", + "rayon", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.19", + "tokio", + "tracing", +] + +[[package]] +name = "ethrex-trie" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "anyhow", + "bytes", + "crossbeam", + "ethereum-types", + "ethrex-crypto", + "ethrex-rlp", + "lazy_static", + "rayon", + "rkyv", + "rustc-hash", + "serde", + "thiserror 2.0.19", +] + +[[package]] +name = "ethrex-vm" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "bytes", + "derive_more", + "dyn-clone", + "ethrex-common", + "ethrex-crypto", + "ethrex-levm", + "ethrex-rlp", + "rustc-hash", + "serde", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.5", + "siphasher", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.7", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hex-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7685beb53fc20efc2605f32f5d51e9ba18b8ef237961d1760169d2290d3bee" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ed8ad1f3877f7e775b8cbf30ed1bd3209a95401817f19a0eb4402d13f8cf90" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lambda-vm-ethrex-crypto" +version = "0.1.0" +dependencies = [ + "ethrex-crypto", + "k256", + "lambda-vm-syscalls", +] + +[[package]] +name = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand 0.9.5", + "riscv", + "thiserror 1.0.69", +] + +[[package]] +name = "lambdaworks-crypto" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b1a1c1102a5a7fbbda117b79fb3a01e033459c738a3c1642269603484fd1c1" +dependencies = [ + "lambdaworks-math", + "rand 0.8.7", + "rand_chacha 0.3.1", + "serde", + "sha2", + "sha3", +] + +[[package]] +name = "lambdaworks-math" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "018a95aa873eb49896a858dee0d925c33f3978d073c64b08dd4f2c9b35a017c6" +dependencies = [ + "getrandom 0.2.17", + "num-bigint", + "num-traits", + "rand 0.8.7", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "malachite" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec410515e231332b14cd986a475d1c3323bcfa4c7efc038bfa1d5b410b1c57e4" +dependencies = [ + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34" +dependencies = [ + "hashbrown 0.15.5", + "itertools 0.14.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-nz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b" +dependencies = [ + "itertools 0.14.0", + "libm", + "malachite-base", + "wide", +] + +[[package]] +name = "malachite-q" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675" +dependencies = [ + "itertools 0.14.0", + "malachite-base", + "malachite-nz", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 2.0.19", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "rlp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rlsf" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07393724337be2ee43a9d86164df4505746874a3fa65913374bc6d6a92314362" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.7", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "snap" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spawned-concurrency" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc21166874e8cd7584ea795c223303160461f0bb1b571bc23e92ca2abb7c5149" +dependencies = [ + "futures", + "pin-project-lite", + "spawned-macros", + "spawned-rt", + "thiserror 2.0.19", + "tracing", +] + +[[package]] +name = "spawned-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64742b41741dfebd5b5ba4dbc4cbc5cc91f4a2cf8107191007d64295682973" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "spawned-rt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e270e6606a118708120671f2d171316762fa832cab73699c714c23aaafe6eb" +dependencies = [ + "ctrlc", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-real-block/Cargo.toml new file mode 100644 index 000000000..87d565a60 --- /dev/null +++ b/tooling/ethrex-real-block/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ethrex-real-block" +version = "0.1.0" +edition = "2024" + +# Detached workspace: keeps the heavy ethrex host deps out of the main build. +[workspace] + +[dependencies] +# Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch) +# so the generated ProgramInput rkyv layout matches what the guest deserializes. +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common" } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config" } +# `default-features = false, features = ["lambdavm"]` mirrors the guest +# (executor/programs/rust/ethrex/Cargo.toml) exactly, so the parity test below +# executes the block against the same precompile surface the guest has β€” most +# importantly WITHOUT KZG, which `lambdavm` does not link (only `sp1` pulls +# `ethrex-crypto/kzg-rs`). Under the default `secp256k1` feature the test would +# silently exercise a richer set of precompiles than the guest actually ships. +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } + +# Exact pin: the fixture writer and the guest/executor readers must agree on the +# rkyv layout. Keep this in sync with executor/{Cargo.toml,programs/rust/ethrex/Cargo.toml}. +rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +# The very `Crypto` impl the guest injects. On host its keccak/ECSM accelerator +# paths fall back to the same pure-Rust routines, so executing with it here +# reproduces the guest's crypto semantics without needing an RV64 toolchain. +lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } + +# `ethrex-guest-program`'s `lambdavm` feature pins `lambda-vm-syscalls` to an +# older commit; override it with our working-tree copy, as the guest does. +[patch."https://github.com/yetanotherco/lambda_vm.git"] +lambda-vm-syscalls = { path = "../../syscalls" } diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md new file mode 100644 index 000000000..5602f0a5c --- /dev/null +++ b/tooling/ethrex-real-block/README.md @@ -0,0 +1,144 @@ +# ethrex-real-block + +Converts a **real Ethereum block** into a serialized `ProgramInput` `.bin` for +the lambda-vm ethrex guest, reading an [`ethrex-replay`][replay] cache JSON. + +This complements `tooling/ethrex-fixtures`, which builds *synthetic* blocks of N +plain ETH transfers. Those are cheap and deterministic but not representative: +they execute no contract code, touch a genesis state trie only a couple of +levels deep, and carry no bytecode in the witness. This tool produces the +opposite β€” a block that actually looks like Ethereum. + +| | `ethrex_bench_20.bin` (synthetic) | `ethrex_hoodi_1265656.bin` (real) | +|---|---|---| +| gas used | 420,000 | **4,402,947** | +| transactions | 20 (all plain transfers) | 11 (7Γ— EIP-1559, 4Γ— EIP-4844 blob) | +| contract calls | 0 | 5, at ~830–955k gas each | +| contract bytecode in witness | 0 | 22 contracts, ~124 KB | +| state-trie nodes | a handful | 1,705 | +| storage keys | 0 | 422 | +| serialized size | 17 KB | 1,021,207 B | + +Note it has *fewer* transactions than the synthetic fixture while being far more +representative β€” transaction count is not the axis that matters. + +Block 1265656 is **verified to run on the guest's precompile surface** (see +[Validation](#validation)); it needs no accelerator we don't have. Any +replacement block must clear the same check β€” that is what makes it usable, not +just realistic. + +## Prerequisites +Rust (stable) and network access on first run (cargo fetches the pinned ethrex +crates; `make` downloads the cache). **No RV64 target or sysroot needed** β€” this +is a host tool. + +## How to run + +From the repo root, the committed default (Hoodi block 1265656): + +```bash +make ethrex-real-block-fixture +``` + +That downloads the cache to `caches/` (gitignored) and writes +`executor/tests/ethrex_hoodi_1265656.bin` (gitignored β€” ~1 MB, too large to +commit; see `executor/.gitignore`). + +The cache URL is pinned to an ethrex-replay **commit**, not to `main` +(`ETHREX_REPLAY_REV` in the Makefile), matching how the guest pins ethrex. A +branch ref would let the benchmark's input change under a fixed fixture name, +which would quietly destroy comparability between runs. + +Directly, against any cache file: + +```bash +cd tooling/ethrex-real-block +cargo run --release -- +``` + +Output is deterministic for a given cache file: + +```text +ethrex_hoodi_1265656.bin + block: hoodi #1265656 β€” 11 transactions, 4,402,947 gas + sha256: 1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142 + source: ethrex-replay caches/cache_hoodi_1265656.json @ 2693e018 +``` + +That checksum is documentation of what the fixture should be, not an enforced +gate β€” the pinned commit is what makes the input immutable, and +`conversion_is_reproducible` pins the block's stats and serialized length. + +## Getting a cache for a different block + +The cache format is `ethrex-replay`'s, so use that tool to produce one β€” it +handles the RPC fetching, multiple client backends, and the `eth_getProof` +fallback, none of which is worth reimplementing here: + +```bash +# In a checkout of https://github.com/lambdaclass/ethrex-replay +ethrex-replay cache --rpc-url +``` + +`debug_executionWitness` requires a **reth or ethrex** node; public providers +(Alchemy, Infura) do not serve it. `ethrex-replay` also supports `eth_getProof` +for geth/nethermind. + +Then point this tool at the resulting JSON. To make a new block the default, +override `ETHREX_REAL_BLOCK` (Makefile) or add a rule alongside it. + +## Why the JSON and not ethrex-replay's own `.bin` + +`ethrex-replay` can already emit a rkyv `ProgramInput`, but it tracks ethrex +`main`, where the type has diverged from the rev our guest pins +(`156cb8d6…`): `main` has an extra `fee_configs` field, moved the type from +`l1::` to `input::`, and uses rkyv 0.8.10 against our exact `=0.8.16`. Its +binary would not deserialize in our guest. + +The cache JSON carries only `blocks` + `witness` + `network` as plain serde, so +it survives that drift. This tool re-reads it with **our** pinned ethrex types +and re-serializes with **our** rkyv, which is what keeps the output layout +correct by construction. When the guest's ethrex `rev` is bumped, bump it here +too (and in `tooling/ethrex-fixtures` and the guest) and regenerate. + +A previous real-block fixture (`ethrex_hoodi.bin`) was lost exactly to this kind +of drift β€” it predated the `Crypto` trait and stopped deserializing. Reading the +version-tolerant JSON instead of a pinned binary is the mitigation. + +## Validation + +Three checks, cheapest first. The first two run on the host in milliseconds and +need no RV64 toolchain. + +**`cargo test` here β€” `real_block_executes_under_guest_crypto`.** The screen for +*"does this block need an accelerator we don't have?"*. It builds +`ethrex-guest-program` with `default-features = false, features = ["lambdavm"]` +and executes with `LambdaVmEcsmCrypto` β€” byte-for-byte the guest's own +configuration. Stateless re-execution ends in a post-state-root check, so a +block reaching a precompile the guest doesn't link diverges from consensus and +fails here. KZG point evaluation (0x0a) is the notable omission: `lambdavm` does +not pull `ethrex-crypto/kzg-rs` (only `sp1` does). + +Getting this configuration right matters. Under the default `secp256k1` feature +the same test passes while exercising a *richer* precompile set than the guest +ships β€” green, and meaningless. + +**`cargo test` here β€” `conversion_is_reproducible`.** Pins the block's stats and +the serialized length, so an ethrex rev bump that moves the rkyv layout is +caught rather than silently producing a fixture the guest can't read. + +**`tooling/ethrex-tests` β€” `test_ethrex_real_block_native`.** Checks the +serialized `.bin` itself deserializes and executes. That crate builds +`ethrex-guest-program` with default features, so this covers the artifact, not +the guest's precompile surface. + +**`tooling/ethrex-tests` β€” `test_ethrex_real_block_vm`** (`#[ignore]`, excluded +from PR CI). The block through the guest ELF, comparing the VM's committed +output against the native reference. Needs the RV64 toolchain and its runtime is +unmeasured; run it on a build server, not a laptop: + +```bash +cd tooling/ethrex-tests && cargo test --release test_ethrex_real_block_vm -- --ignored +``` + +[replay]: https://github.com/lambdaclass/ethrex-replay diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs new file mode 100644 index 000000000..5bd03aed0 --- /dev/null +++ b/tooling/ethrex-real-block/src/main.rs @@ -0,0 +1,136 @@ +//! Convert a real Ethereum block into the rkyv-serialized `ProgramInput` the +//! lambda-vm ethrex guest consumes, from an `ethrex-replay` cache JSON. +//! +//! Usage: +//! cargo run --release -- + +use ethrex_common::types::Block; +use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_config::networks::Network; +use ethrex_guest_program::l1::ProgramInput; +use serde::Deserialize; + +/// The subset of `ethrex-replay`'s on-disk cache that a `ProgramInput` needs. +/// +/// Deliberately deserialized with *our* pinned ethrex types rather than by +/// depending on `ethrex-replay`: it tracks ethrex `main`, where `ProgramInput` +/// has diverged (extra `fee_configs` field, different module path, rkyv 0.8.10 +/// vs our `=0.8.16`), so its own `.bin` output would not deserialize in our +/// guest. This JSON is the version-tolerant interface between the two. +/// +/// Extra fields in the file (L2 blob data, custom `chain_config`) are ignored. +#[derive(Deserialize)] +struct Cache { + blocks: Vec, + witness: RpcExecutionWitness, + network: Network, +} + +/// Summary of the converted block, for the CLI's one-line report. +struct BlockSummary { + network: String, + first_block_number: u64, + blocks: usize, + transactions: usize, + gas_used: u64, +} + +fn program_input_from_cache( + cache_path: &str, +) -> Result<(ProgramInput, BlockSummary), Box> { + let cache: Cache = + serde_json::from_reader(std::io::BufReader::new(std::fs::File::open(cache_path)?))?; + + let Some(first_block) = cache.blocks.first() else { + return Err("cache contains no blocks".into()); + }; + let summary = BlockSummary { + network: cache.network.to_string(), + first_block_number: first_block.header.number, + blocks: cache.blocks.len(), + transactions: cache.blocks.iter().map(|b| b.body.transactions.len()).sum(), + gas_used: cache.blocks.iter().map(|b| b.header.gas_used).sum(), + }; + + // `into_execution_witness` rebuilds the trie structures from the flat node + // list and needs the parent header, which the cache carries in `headers`. + let chain_config = cache.network.get_genesis()?.config; + let witness = cache + .witness + .into_execution_witness(chain_config, summary.first_block_number)?; + + Ok((ProgramInput::new(cache.blocks, witness), summary)) +} + +fn usage_and_exit(program: &str) -> ! { + eprintln!("usage: {program} "); + std::process::exit(2); +} + +fn main() -> Result<(), Box> { + let mut args = std::env::args(); + let program = args.next().unwrap_or_else(|| "ethrex-real-block".into()); + let (Some(cache_path), Some(out_path)) = (args.next(), args.next()) else { + usage_and_exit(&program); + }; + if args.next().is_some() { + usage_and_exit(&program); + } + + let (program_input, summary) = program_input_from_cache(&cache_path)?; + let bytes = rkyv::to_bytes::(&program_input)?; + std::fs::write(&out_path, &bytes)?; + + println!( + "wrote {out_path} ({} bytes): {} block(s) from {} starting at #{}, \ + {} transaction(s), {} gas", + bytes.len(), + summary.blocks, + summary.network, + summary.first_block_number, + summary.transactions, + summary.gas_used, + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const CACHE: &str = "caches/cache_hoodi_1265656.json"; + + /// Executes the converted block against the guest's exact precompile + /// surface: `ethrex-guest-program` with `default-features = false, + /// features = ["lambdavm"]` (see Cargo.toml) plus `LambdaVmEcsmCrypto`, + /// the same `Crypto` impl the guest injects. + /// + /// This is the screen for "does the block need an accelerator we don't + /// have". Stateless re-execution ends in a post-state-root check, so a + /// block reaching an unlinked precompile β€” KZG point evaluation (0x0a) is + /// the one `lambdavm` omits β€” diverges from consensus and fails here, + /// on the host, without needing an RV64 toolchain or a proving run. + #[test] + fn real_block_executes_under_guest_crypto() { + use ethrex_guest_program::l1::execution_program; + use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; + use std::sync::Arc; + + let (program_input, _) = program_input_from_cache(CACHE).unwrap(); + execution_program(program_input, Arc::new(LambdaVmEcsmCrypto)).unwrap(); + } + + /// The serialized form is what the guest actually reads, so pin it: a + /// change here means the rkyv layout moved and every consumer of the + /// fixture (and its README checksum) needs regenerating. + #[test] + fn conversion_is_reproducible() { + let (program_input, summary) = program_input_from_cache(CACHE).unwrap(); + let bytes = rkyv::to_bytes::(&program_input).unwrap(); + + assert_eq!(summary.first_block_number, 1_265_656); + assert_eq!(summary.transactions, 11); + assert_eq!(summary.gas_used, 4_402_947); + assert_eq!(bytes.len(), 1_021_207); + } +} diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index c87ccceba..72f555ad9 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -86,3 +86,53 @@ fn test_ethrex_empty_block() { let output = execution_program(input, Arc::new(NativeCrypto)).unwrap(); run_program_and_check_public_output(ELF_PATH, output.encode(), inputs); } + +const REAL_BLOCK_FIXTURE: &str = "ethrex_hoodi_1265656.bin"; + +/// Host-only acceptance gate for the real-block fixture produced by +/// `tooling/ethrex-real-block` (`make ethrex-real-block-fixture`): the block is +/// re-executed statelessly against its own witness, so a successful run means +/// the recovered tries, codes and headers reproduce the header's post-state +/// root. Needs no guest ELF, which is what keeps it runnable where the RV64 +/// toolchain isn't available. +/// +/// Checks the *serialized artifact* specifically β€” that the committed rkyv +/// bytes deserialize and execute β€” which is why it reads the `.bin` rather +/// than converting the cache itself. It runs under `NativeCrypto` (ethrex's +/// full precompile set) because this crate builds `ethrex-guest-program` with +/// default features, so it does NOT speak to the guest's reduced surface; +/// `tooling/ethrex-real-block`'s `real_block_executes_under_guest_crypto` +/// covers that, under `lambdavm` features + `LambdaVmEcsmCrypto`. +#[test] +fn test_ethrex_real_block_native() { + use ethrex_guest_program::crypto::NativeCrypto; + use ethrex_guest_program::l1::{ProgramInput, execution_program}; + use rkyv::rancor::Error; + use std::sync::Arc; + let inputs = std::fs::read(format!("{FIXTURES_DIR}/{REAL_BLOCK_FIXTURE}")).unwrap(); + let input = rkyv::from_bytes::(&inputs).unwrap(); + execution_program(input, Arc::new(NativeCrypto)).unwrap(); +} + +/// The same real block through the guest ELF, checking the VM's committed +/// output matches the native reference. Split from the native gate above +/// because this one needs the ethrex ELF and is far heavier than the synthetic +/// fixtures (a ~1 MB witness, real contract execution). +/// +/// Deliberately excluded from the PR CI step, which otherwise runs everything +/// via `--include-ignored`: the cycle cost of a real block in the VM has not +/// been measured yet, so it is opt-in until we know what it does to job time. +/// Run it explicitly with: +/// cd tooling/ethrex-tests && cargo test --release test_ethrex_real_block_vm -- --ignored +#[ignore = "real block through the VM; unmeasured runtime, run explicitly on a build server"] +#[test] +fn test_ethrex_real_block_vm() { + use ethrex_guest_program::crypto::NativeCrypto; + use ethrex_guest_program::l1::{ProgramInput, execution_program}; + use rkyv::rancor::Error; + use std::sync::Arc; + let inputs = std::fs::read(format!("{FIXTURES_DIR}/{REAL_BLOCK_FIXTURE}")).unwrap(); + let input = rkyv::from_bytes::(&inputs).unwrap(); + let output = execution_program(input, Arc::new(NativeCrypto)).unwrap(); + run_program_and_check_public_output(ELF_PATH, output.encode(), inputs); +} From a5765113edb586e4aff6d4dad169b403cd369049 Mon Sep 17 00:00:00 2001 From: Nicole Date: Thu, 30 Jul 2026 12:48:54 -0300 Subject: [PATCH 02/27] Add default-features=false, make curl download atomic, pin fixture by sha256 not length, skip heavy VM test in local target, and add offline variant --- Makefile | 13 +++++++++---- tooling/ethrex-real-block/Cargo.toml | 5 +++-- tooling/ethrex-real-block/src/main.rs | 13 +++++++++++-- tooling/ethrex-tests/tests/ethrex.rs | 4 ++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index a95f9f49a..f7ec69cb1 100644 --- a/Makefile +++ b/Makefile @@ -279,7 +279,7 @@ test-rust: compile-programs-rust # The source is ethrex-replay's cache JSON, not that tool's own rkyv output: # ethrex-replay tracks ethrex `main`, where `ProgramInput` has diverged from the # rev our guest pins, so only the JSON is safe to read across the two. -ETHREX_REAL_BLOCK ?= 1265656 +ETHREX_REAL_BLOCK := 1265656 # Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would # let the benchmark's input change under a fixed fixture name, silently breaking # comparability across runs. Re-pin deliberately when adopting a new block. @@ -291,17 +291,22 @@ ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) $(ETHREX_REAL_BLOCK_CACHE): mkdir -p $(dir $@) - curl -fsSL -o $@ \ + curl -fsSL -o $@.tmp \ https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json + mv $@.tmp $@ -$(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-block/src/main.rs +$(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-block/src/main.rs tooling/ethrex-real-block/Cargo.toml tooling/ethrex-real-block/Cargo.lock cd tooling/ethrex-real-block && \ cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$@ # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock). test-ethrex: compile-programs-rust $(ETHREX_REAL_BLOCK_FIXTURE) - cd tooling/ethrex-tests && cargo test --release -- --include-ignored + cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm + +# Offline variant: skips the real-block fixture (no network required). +test-ethrex-offline: compile-programs-rust + cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm test-flamegraph: cargo test -p executor --test flamegraph diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-real-block/Cargo.toml index 87d565a60..36997ebab 100644 --- a/tooling/ethrex-real-block/Cargo.toml +++ b/tooling/ethrex-real-block/Cargo.toml @@ -9,8 +9,8 @@ edition = "2024" [dependencies] # Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch) # so the generated ProgramInput rkyv layout matches what the guest deserializes. -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common" } -ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config" } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common", default-features = false } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config", default-features = false } # `default-features = false, features = ["lambdavm"]` mirrors the guest # (executor/programs/rust/ethrex/Cargo.toml) exactly, so the parity test below # executes the block against the same precompile surface the guest has β€” most @@ -30,6 +30,7 @@ serde_json = "1" # paths fall back to the same pure-Rust routines, so executing with it here # reproduces the guest's crypto semantics without needing an RV64 toolchain. lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } +sha2 = "0.10" # `ethrex-guest-program`'s `lambdavm` feature pins `lambda-vm-syscalls` to an # older commit; override it with our working-tree copy, as the guest does. diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index 5bd03aed0..55188ec81 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -53,7 +53,7 @@ fn program_input_from_cache( }; // `into_execution_witness` rebuilds the trie structures from the flat node - // list and needs the parent header, which the cache carries in `headers`. + // list and needs the parent header, which the cache carries inside `witness`. let chain_config = cache.network.get_genesis()?.config; let witness = cache .witness @@ -131,6 +131,15 @@ mod tests { assert_eq!(summary.first_block_number, 1_265_656); assert_eq!(summary.transactions, 11); assert_eq!(summary.gas_used, 4_402_947); - assert_eq!(bytes.len(), 1_021_207); + + // Pin the exact serialized bytes, not just length: HashMap iteration order + // could vary the output while keeping size fixed. + let expected: [u8; 32] = [ + 0x1f, 0x7d, 0x4c, 0x4c, 0xdf, 0x9b, 0xd5, 0x24, + 0x72, 0xd9, 0xeb, 0xaf, 0xdb, 0x40, 0x38, 0xf5, + 0x7a, 0x88, 0xc3, 0xc9, 0x2d, 0x65, 0xc9, 0x6f, + 0xd8, 0x6d, 0x7e, 0x32, 0x3d, 0xb8, 0x71, 0x42, + ]; + assert_eq!(sha2::Sha256::digest(&bytes).as_slice(), expected); } } diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 72f555ad9..7696fb852 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -40,8 +40,8 @@ const ELF_PATH: &str = "../../executor/program_artifacts/rust/ethrex.elf"; const FIXTURES_DIR: &str = "../../executor/tests"; /// Larger-block smoke test: a synthetic ethrex block with 10 ETH transfers. -/// (Replaces the old `ethrex_hoodi.bin` real-block fixture, which was in the -/// pre-Crypto-trait ethrex format and no longer deserializes.) +/// (The old `ethrex_hoodi.bin` real-block fixture predated the `Crypto` trait +/// and no longer deserializes; the current real-block fixture is separate.) #[ignore = "heavier synthetic block (10 txs); run in the dedicated --ignored CI step"] #[test] fn test_ethrex() { From ef7a4f1dd43e6954bcde8474899d8473a077974f Mon Sep 17 00:00:00 2001 From: Nicole Date: Thu, 30 Jul 2026 14:22:29 -0300 Subject: [PATCH 03/27] fix missing import, assert the fixture's digest instead of its length and correct doc --- .github/workflows/pr_main.yaml | 7 +++-- tooling/ethrex-real-block/Cargo.lock | 4 +-- tooling/ethrex-real-block/Cargo.toml | 22 +++++++------ tooling/ethrex-real-block/README.md | 32 ++++++++++++------- tooling/ethrex-real-block/src/main.rs | 45 ++++++++++++++++----------- tooling/ethrex-tests/tests/ethrex.rs | 31 +++++++++++++++--- 6 files changed, 92 insertions(+), 49 deletions(-) diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index f6460cd2c..be4c4a5c6 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -107,9 +107,10 @@ jobs: - name: Generate ethrex real-block fixture run: make ethrex-real-block-fixture - # Detached workspace, and the only place the block is executed against the - # guest's actual precompile surface (lambdavm features + LambdaVmEcsmCrypto), - # so it is what catches a block needing an accelerator we don't have. + # Detached workspace. Executes the block through `LambdaVmEcsmCrypto`, the + # guest's own `Crypto` impl, and pins the fixture's sha256. Note it does NOT + # screen KZG (its graph links c-kzg via ethrex-config β€” see that crate's + # Cargo.toml); the ethrex-tests step below is what covers 0x0a. - name: Run ethrex real-block converter tests (detached workspace) run: | cd tooling/ethrex-real-block && cargo test --release diff --git a/tooling/ethrex-real-block/Cargo.lock b/tooling/ethrex-real-block/Cargo.lock index dc1c04d3e..647596e66 100644 --- a/tooling/ethrex-real-block/Cargo.lock +++ b/tooling/ethrex-real-block/Cargo.lock @@ -918,10 +918,8 @@ dependencies = [ "libc", "lru", "once_cell", - "rayon", "rkyv", "rustc-hash", - "secp256k1", "serde", "serde_json", "sha2", @@ -959,7 +957,6 @@ dependencies = [ "num-bigint", "p256", "ripemd", - "secp256k1", "sha2", "thiserror 2.0.19", "tiny-keccak", @@ -1087,6 +1084,7 @@ dependencies = [ "rkyv", "serde", "serde_json", + "sha2", ] [[package]] diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-real-block/Cargo.toml index 36997ebab..2a37b4bcb 100644 --- a/tooling/ethrex-real-block/Cargo.toml +++ b/tooling/ethrex-real-block/Cargo.toml @@ -11,12 +11,16 @@ edition = "2024" # so the generated ProgramInput rkyv layout matches what the guest deserializes. ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common", default-features = false } ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config", default-features = false } -# `default-features = false, features = ["lambdavm"]` mirrors the guest -# (executor/programs/rust/ethrex/Cargo.toml) exactly, so the parity test below -# executes the block against the same precompile surface the guest has β€” most -# importantly WITHOUT KZG, which `lambdavm` does not link (only `sp1` pulls -# `ethrex-crypto/kzg-rs`). Under the default `secp256k1` feature the test would -# silently exercise a richer set of precompiles than the guest actually ships. +# Matches the guest's own declaration (executor/programs/rust/ethrex/Cargo.toml). +# +# CAVEAT: this line alone does NOT reproduce the guest's precompile surface. The +# `ethrex-config` dep above pulls `ethrex-p2p`, whose `default = ["c-kzg"]` +# propagates to `ethrex-crypto/c-kzg` β€” and `default-features = false` cannot +# switch it off, because ethrex's own workspace declares `ethrex-p2p` with its +# defaults on. So this graph links a working c-kzg (and malachite modexp, and +# `ark-ff/asm` BN254) that the guest does not have; verify with +# `cargo tree -e features -i ethrex-crypto`. Closing that gap means dropping +# `ethrex-config` and sourcing `ChainConfig` another way β€” see the README. ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } # Exact pin: the fixture writer and the guest/executor readers must agree on the @@ -26,9 +30,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" [dev-dependencies] -# The very `Crypto` impl the guest injects. On host its keccak/ECSM accelerator -# paths fall back to the same pure-Rust routines, so executing with it here -# reproduces the guest's crypto semantics without needing an RV64 toolchain. +# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths +# both fall back to software, so this exercises the guest's *fallback* crypto +# semantics β€” not the accelerators themselves. lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } sha2 = "0.10" diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 5602f0a5c..96f909a17 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -110,18 +110,28 @@ version-tolerant JSON instead of a pinned binary is the mitigation. Three checks, cheapest first. The first two run on the host in milliseconds and need no RV64 toolchain. -**`cargo test` here β€” `real_block_executes_under_guest_crypto`.** The screen for -*"does this block need an accelerator we don't have?"*. It builds +**`cargo test` here β€” `real_block_executes_under_guest_crypto`.** Executes the +block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is +exercised via the guest's own trait dispatch. Stateless re-execution ends in a +post-state-root check, so any divergence from consensus fails here. + +**It is not a full guest-precompile screen.** Declaring `ethrex-guest-program` with `default-features = false, features = ["lambdavm"]` -and executes with `LambdaVmEcsmCrypto` β€” byte-for-byte the guest's own -configuration. Stateless re-execution ends in a post-state-root check, so a -block reaching a precompile the guest doesn't link diverges from consensus and -fails here. KZG point evaluation (0x0a) is the notable omission: `lambdavm` does -not pull `ethrex-crypto/kzg-rs` (only `sp1` does). - -Getting this configuration right matters. Under the default `secp256k1` feature -the same test passes while exercising a *richer* precompile set than the guest -ships β€” green, and meaningless. +is necessary but not sufficient: the `ethrex-config` dependency (used only for +`Network::get_genesis()`) pulls `ethrex-p2p`, whose `default = ["c-kzg"]` +propagates down to `ethrex-crypto/c-kzg`, and `default-features = false` cannot +switch it off because ethrex's own workspace declares `ethrex-p2p` with defaults +on. Verify with `cargo tree -e features -i ethrex-crypto`. Consequences: KZG +point evaluation (0x0a) resolves to a working c-kzg here but to nothing in the +guest; modexp runs malachite here and num-bigint there; BN254 gets `ark-ff/asm`. + +Closing that gap means dropping `ethrex-config` and sourcing `ChainConfig` +another way, which is a design change β€” tracked as follow-up, not done here. + +**What screens 0x0a today** is `test_ethrex_real_block_native` in +`tooling/ethrex-tests`, whose graph links no KZG backend at all. That was +incidental to its dependencies, so `no_kzg_backend_linked` in the same file now +asserts it and will go red if anything pulls a backend in. **`cargo test` here β€” `conversion_is_reproducible`.** Pins the block's stats and the serialized length, so an ethrex rev bump that moves the rkyv layout is diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index 55188ec81..cabeaaa8a 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -100,16 +100,21 @@ mod tests { const CACHE: &str = "caches/cache_hoodi_1265656.json"; - /// Executes the converted block against the guest's exact precompile - /// surface: `ethrex-guest-program` with `default-features = false, - /// features = ["lambdavm"]` (see Cargo.toml) plus `LambdaVmEcsmCrypto`, - /// the same `Crypto` impl the guest injects. + /// Executes the converted block with `LambdaVmEcsmCrypto`, the `Crypto` impl + /// the guest injects, so the block is exercised through the same trait + /// dispatch the guest uses. Stateless re-execution ends in a post-state-root + /// check, so any divergence from consensus fails here β€” on the host, with no + /// RV64 toolchain and no proving run. /// - /// This is the screen for "does the block need an accelerator we don't - /// have". Stateless re-execution ends in a post-state-root check, so a - /// block reaching an unlinked precompile β€” KZG point evaluation (0x0a) is - /// the one `lambdavm` omits β€” diverges from consensus and fails here, - /// on the host, without needing an RV64 toolchain or a proving run. + /// This is NOT a complete guest-precompile screen, despite what an earlier + /// version of this comment claimed. This crate's dependency graph links + /// `c-kzg` (via `ethrex-config` β†’ `ethrex-p2p`, see Cargo.toml), so KZG point + /// evaluation (0x0a) resolves to a working implementation here while the + /// guest has none. A block calling 0x0a would pass this test. + /// + /// What does screen 0x0a today is `test_ethrex_real_block_native` in + /// `tooling/ethrex-tests`, whose graph happens to link no KZG backend at all + /// β€” accidentally, and enforced by `no_kzg_backend_linked` there. #[test] fn real_block_executes_under_guest_crypto() { use ethrex_guest_program::l1::execution_program; @@ -125,6 +130,8 @@ mod tests { /// fixture (and its README checksum) needs regenerating. #[test] fn conversion_is_reproducible() { + use sha2::Digest; + let (program_input, summary) = program_input_from_cache(CACHE).unwrap(); let bytes = rkyv::to_bytes::(&program_input).unwrap(); @@ -132,14 +139,16 @@ mod tests { assert_eq!(summary.transactions, 11); assert_eq!(summary.gas_used, 4_402_947); - // Pin the exact serialized bytes, not just length: HashMap iteration order - // could vary the output while keeping size fixed. - let expected: [u8; 32] = [ - 0x1f, 0x7d, 0x4c, 0x4c, 0xdf, 0x9b, 0xd5, 0x24, - 0x72, 0xd9, 0xeb, 0xaf, 0xdb, 0x40, 0x38, 0xf5, - 0x7a, 0x88, 0xc3, 0xc9, 0x2d, 0x65, 0xc9, 0x6f, - 0xd8, 0x6d, 0x7e, 0x32, 0x3d, 0xb8, 0x71, 0x42, - ]; - assert_eq!(sha2::Sha256::digest(&bytes).as_slice(), expected); + // Digest, not length: a layout change can preserve the byte count exactly + // (`ChainConfig` is fixed-size, and rkyv's `big_endian` feature would only + // byte-swap in place), so `len()` cannot pin the archived form. + let digest: String = sha2::Sha256::digest(&bytes) + .iter() + .map(|b| format!("{b:02x}")) + .collect(); + assert_eq!( + digest, "1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142", + "fixture bytes changed β€” regenerate it and update the README checksum", + ); } } diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 7696fb852..335417fa2 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -98,11 +98,14 @@ const REAL_BLOCK_FIXTURE: &str = "ethrex_hoodi_1265656.bin"; /// /// Checks the *serialized artifact* specifically β€” that the committed rkyv /// bytes deserialize and execute β€” which is why it reads the `.bin` rather -/// than converting the cache itself. It runs under `NativeCrypto` (ethrex's -/// full precompile set) because this crate builds `ethrex-guest-program` with -/// default features, so it does NOT speak to the guest's reduced surface; -/// `tooling/ethrex-real-block`'s `real_block_executes_under_guest_crypto` -/// covers that, under `lambdavm` features + `LambdaVmEcsmCrypto`. +/// than converting the cache itself. +/// +/// It is also, in practice, the check that the block needs no KZG: this crate's +/// dependency graph links no KZG backend, so a block calling point evaluation +/// (0x0a) diverges from consensus here and fails. That property is incidental to +/// the dep graph rather than declared, so `no_kzg_backend_linked` below pins it. +/// (`tooling/ethrex-real-block`'s parity test does NOT cover 0x0a β€” it links +/// `c-kzg` transitively via `ethrex-config`.) #[test] fn test_ethrex_real_block_native() { use ethrex_guest_program::crypto::NativeCrypto; @@ -114,6 +117,24 @@ fn test_ethrex_real_block_native() { execution_program(input, Arc::new(NativeCrypto)).unwrap(); } +/// Pins the property the test above leans on: this crate's dependency graph must +/// link no KZG backend, so a block calling point evaluation (0x0a) diverges from +/// consensus and fails rather than passing on a surface the guest doesn't have. +/// If a future dependency pulls `c-kzg` or `kzg-rs` in, this goes red instead of +/// the screen silently disappearing. +#[test] +fn no_kzg_backend_linked() { + use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; + let err = NativeCrypto + .verify_kzg_proof(&[0u8; 32], &[0u8; 32], &[0u8; 48], &[0u8; 48]) + .expect_err("a KZG backend is linked: verify_kzg_proof succeeded on zero input"); + assert!( + format!("{err:?}").contains("unimplemented"), + "a KZG backend got linked into ethrex-tests, so test_ethrex_real_block_native \ + no longer screens precompile 0x0a: {err:?}" + ); +} + /// The same real block through the guest ELF, checking the VM's committed /// output matches the native reference. Split from the native gate above /// because this one needs the ethrex ELF and is far heavier than the synthetic From 99b95ff91731599b92cffb75258b76fdc3e4083e Mon Sep 17 00:00:00 2001 From: Nicole Date: Thu, 30 Jul 2026 15:00:53 -0300 Subject: [PATCH 04/27] Reject caches from unrecognised networks --- tooling/ethrex-real-block/README.md | 7 +++++ tooling/ethrex-real-block/src/main.rs | 40 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 96f909a17..d600495fc 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -84,6 +84,13 @@ ethrex-replay cache --rpc-url (Alchemy, Infura) do not serve it. `ethrex-replay` also supports `eth_getProof` for geth/nethermind. +Only **mainnet, Hoodi and Sepolia** caches are accepted. ethrex-replay writes +`network: "LocalDevnet"` for any other chain, and that resolves to a test chain +(chain_id 9, every fork active from timestamp 0) β€” so converting it would replay +the block under invented rules while still passing every check here, since the +witness is only ever validated against whichever config we chose. The converter +refuses instead; `unmappable_network_is_rejected` pins that. + Then point this tool at the resulting JSON. To make a new block the default, override `ETHREX_REAL_BLOCK` (Makefile) or add a rule alongside it. diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index cabeaaa8a..fbbc5af95 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -52,6 +52,22 @@ fn program_input_from_cache( gas_used: cache.blocks.iter().map(|b| b.header.gas_used).sum(), }; + // The chain rules come from `network`, and ethrex-replay maps every chain it + // doesn't recognise (anything but mainnet / Hoodi / Sepolia) onto + // `LocalDevnet` β€” which resolves to a test chain: chain_id 9, every fork + // active from timestamp 0. Converting under that would execute the block + // against invented rules while still satisfying every check downstream (the + // witness is replayed against whatever config we picked, and host and guest + // read the same one), so refuse rather than guess. + if !matches!(cache.network, Network::PublicNetwork(_)) { + return Err(format!( + "unsupported network `{}`: its chain rules would be guessed, not read \ + (ethrex-replay writes LocalDevnet for any chain it does not recognise)", + cache.network + ) + .into()); + } + // `into_execution_witness` rebuilds the trie structures from the flat node // list and needs the parent header, which the cache carries inside `witness`. let chain_config = cache.network.get_genesis()?.config; @@ -125,6 +141,30 @@ mod tests { execution_program(program_input, Arc::new(LambdaVmEcsmCrypto)).unwrap(); } + /// A cache whose `network` we can't map to real chain rules must be refused, + /// not converted under substituted ones. Only the `network` field is changed + /// here, and the result is byte-length-identical to the real fixture β€” which + /// is exactly why the other tests cannot catch this on their own. + #[test] + fn unmappable_network_is_rejected() { + let mut cache: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(CACHE).unwrap()).unwrap(); + cache["network"] = serde_json::json!("LocalDevnet"); + + let path = std::env::temp_dir().join("ethrex_real_block_localdevnet.json"); + std::fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap(); + let result = program_input_from_cache(path.to_str().unwrap()); + std::fs::remove_file(&path).ok(); + let Err(err) = result else { + panic!("LocalDevnet resolves to chain_id 9 with all forks at 0; must not convert"); + }; + + assert!( + err.to_string().contains("unsupported network"), + "wrong rejection reason: {err}" + ); + } + /// The serialized form is what the guest actually reads, so pin it: a /// change here means the rkyv layout moved and every consumer of the /// fixture (and its README checksum) needs regenerating. From 5c31a794cbe6d19ea3894640ee4274e90d5f30de Mon Sep 17 00:00:00 2001 From: Nicole Date: Thu, 30 Jul 2026 15:57:51 -0300 Subject: [PATCH 05/27] Fix test-ethrex-offline to skip both real-block tests, retry the cache download, and tighten the KZG canary. --- Makefile | 9 +++++--- tooling/ethrex-real-block/README.md | 32 ++++++++++++++++----------- tooling/ethrex-real-block/src/main.rs | 22 ++++++++++-------- tooling/ethrex-tests/tests/ethrex.rs | 23 ++++++++++++++----- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index f7ec69cb1..469845551 100644 --- a/Makefile +++ b/Makefile @@ -291,7 +291,7 @@ ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) $(ETHREX_REAL_BLOCK_CACHE): mkdir -p $(dir $@) - curl -fsSL -o $@.tmp \ + curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json mv $@.tmp $@ @@ -304,9 +304,12 @@ $(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-blo test-ethrex: compile-programs-rust $(ETHREX_REAL_BLOCK_FIXTURE) cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm -# Offline variant: skips the real-block fixture (no network required). +# Offline variant: no network. `--skip test_ethrex_real_block` is a substring +# match, so it drops both real-block tests β€” the `_vm` one and the `_native` one, +# which reads the downloaded fixture and would otherwise fail on a clean checkout. +# The committed synthetic fixtures and `no_kzg_backend_linked` still run. test-ethrex-offline: compile-programs-rust - cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm + cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block test-flamegraph: cargo test -p executor --test flamegraph diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index d600495fc..1a1131dc9 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -91,8 +91,10 @@ the block under invented rules while still passing every check here, since the witness is only ever validated against whichever config we chose. The converter refuses instead; `unmappable_network_is_rejected` pins that. -Then point this tool at the resulting JSON. To make a new block the default, -override `ETHREX_REAL_BLOCK` (Makefile) or add a rule alongside it. +Then point this tool at the resulting JSON. Adopting a different block as the +default is an edit, not a flag: `ETHREX_REAL_BLOCK` and `ETHREX_REPLAY_REV` in the +Makefile, the `hoodi` in the cache URL, `CACHE` and the assertions in +`src/main.rs`, and `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests`. ## Why the JSON and not ethrex-replay's own `.bin` @@ -122,18 +124,22 @@ block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it i exercised via the guest's own trait dispatch. Stateless re-execution ends in a post-state-root check, so any divergence from consensus fails here. -**It is not a full guest-precompile screen.** Declaring -`ethrex-guest-program` with `default-features = false, features = ["lambdavm"]` -is necessary but not sufficient: the `ethrex-config` dependency (used only for +**It does not screen KZG.** Declaring `ethrex-guest-program` with +`default-features = false, features = ["lambdavm"]` is necessary but not +sufficient: the `ethrex-config` dependency (used only for `Network::get_genesis()`) pulls `ethrex-p2p`, whose `default = ["c-kzg"]` -propagates down to `ethrex-crypto/c-kzg`, and `default-features = false` cannot -switch it off because ethrex's own workspace declares `ethrex-p2p` with defaults -on. Verify with `cargo tree -e features -i ethrex-crypto`. Consequences: KZG -point evaluation (0x0a) resolves to a working c-kzg here but to nothing in the -guest; modexp runs malachite here and num-bigint there; BN254 gets `ark-ff/asm`. - -Closing that gap means dropping `ethrex-config` and sourcing `ChainConfig` -another way, which is a design change β€” tracked as follow-up, not done here. +propagates down to `ethrex-crypto/c-kzg` β€” and `default-features = false` cannot +switch it off, because ethrex's own workspace declares `ethrex-p2p` with defaults +on. Verify with `cargo tree -e features -i ethrex-crypto`. So point evaluation +(0x0a) resolves to a working c-kzg here and to nothing in the guest. + +Scope of the gap: in `ethrex-crypto`, KZG is the **only** precompile whose +*availability* is feature-gated. The other two gates swap between working +implementations β€” `secp256k1` picks libsecp256k1 over k256, `std` picks malachite +over num-bigint for modexp β€” so they change which code runs, not whether a block +can execute. Dropping `ethrex-config` would therefore be a CI-time improvement +(it also sheds `ethrex-p2p`, `ethrex-blockchain`, `ethrex-storage` and the c-kzg +and secp256k1 C builds), not a correctness fix. **What screens 0x0a today** is `test_ethrex_real_block_native` in `tooling/ethrex-tests`, whose graph links no KZG backend at all. That was diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index fbbc5af95..72ccee3f6 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -122,15 +122,16 @@ mod tests { /// check, so any divergence from consensus fails here β€” on the host, with no /// RV64 toolchain and no proving run. /// - /// This is NOT a complete guest-precompile screen, despite what an earlier - /// version of this comment claimed. This crate's dependency graph links - /// `c-kzg` (via `ethrex-config` β†’ `ethrex-p2p`, see Cargo.toml), so KZG point - /// evaluation (0x0a) resolves to a working implementation here while the - /// guest has none. A block calling 0x0a would pass this test. + /// It does NOT screen KZG: this crate's graph links `c-kzg` (via + /// `ethrex-config` β†’ `ethrex-p2p`, see Cargo.toml), so point evaluation + /// (0x0a) resolves to a working implementation here while the guest has none. + /// A block calling 0x0a would pass this test. /// - /// What does screen 0x0a today is `test_ethrex_real_block_native` in - /// `tooling/ethrex-tests`, whose graph happens to link no KZG backend at all - /// β€” accidentally, and enforced by `no_kzg_backend_linked` there. + /// `test_ethrex_real_block_native` in `tooling/ethrex-tests` is what covers + /// 0x0a, and `no_kzg_backend_linked` there keeps it covered. That split is + /// sufficient rather than a workaround: KZG is the only precompile in + /// `ethrex-crypto` whose *availability* is feature-gated β€” the other gates + /// swap between two working implementations. #[test] fn real_block_executes_under_guest_crypto() { use ethrex_guest_program::l1::execution_program; @@ -151,7 +152,10 @@ mod tests { serde_json::from_str(&std::fs::read_to_string(CACHE).unwrap()).unwrap(); cache["network"] = serde_json::json!("LocalDevnet"); - let path = std::env::temp_dir().join("ethrex_real_block_localdevnet.json"); + let path = std::env::temp_dir().join(format!( + "ethrex_real_block_localdevnet_{}.json", + std::process::id() + )); std::fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap(); let result = program_input_from_cache(path.to_str().unwrap()); std::fs::remove_file(&path).ok(); diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 335417fa2..b59640a1f 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -122,16 +122,27 @@ fn test_ethrex_real_block_native() { /// consensus and fails rather than passing on a surface the guest doesn't have. /// If a future dependency pulls `c-kzg` or `kzg-rs` in, this goes red instead of /// the screen silently disappearing. +/// Detection is by error *text*, deliberately: zero input fails under a linked +/// backend too (invalid G1 encoding), and both paths surface as +/// `CryptoError::Other`, so the variant can't tell them apart. The string comes +/// from `ethrex-crypto`'s `KzgError::Unimplemented`; if upstream rewords it this +/// test goes red, which is the safe direction. +/// +/// Worth knowing why this can regress: `ethrex-crypto`'s own default feature set +/// is `["std", "kzg-rs", "secp256k1"]`, so any future dependency pulling it in +/// with defaults on restores a backend and silently removes the screen. #[test] fn no_kzg_backend_linked() { use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; - let err = NativeCrypto - .verify_kzg_proof(&[0u8; 32], &[0u8; 32], &[0u8; 48], &[0u8; 48]) - .expect_err("a KZG backend is linked: verify_kzg_proof succeeded on zero input"); + let result = NativeCrypto.verify_kzg_proof(&[0u8; 32], &[0u8; 32], &[0u8; 48], &[0u8; 48]); + let message = match result { + Ok(()) => "verify_kzg_proof accepted zero input".to_string(), + Err(err) => format!("{err:?}"), + }; assert!( - format!("{err:?}").contains("unimplemented"), - "a KZG backend got linked into ethrex-tests, so test_ethrex_real_block_native \ - no longer screens precompile 0x0a: {err:?}" + message.contains("One of features c-kzg, openvm-kzg or kzg-rs should be active"), + "a KZG backend is linked into ethrex-tests, so test_ethrex_real_block_native no \ + longer screens precompile 0x0a: {message}" ); } From f130dcdf621aa8fdfdea7100f95dc9bd5496bab1 Mon Sep 17 00:00:00 2001 From: Nicole Date: Fri, 31 Jul 2026 10:56:02 -0300 Subject: [PATCH 06/27] update readme and add test --- Makefile | 2 +- tooling/ethrex-real-block/README.md | 36 ++++++++++++++++++----------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 469845551..dac63575f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: deps deps-linux deps-macos compile-programs-asm compile-programs-rust compile-bench \ compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \ clean-recursion-elfs clean test test-asm \ -test-rust test-ethrex test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \ +test-rust test-ethrex test-ethrex-offline test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \ test-profile-recursion-block recursion-profile-block-input \ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \ test-prover-cuda test-prover-comprehensive-cuda \ diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 1a1131dc9..c0e5fcf37 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -65,9 +65,9 @@ ethrex_hoodi_1265656.bin source: ethrex-replay caches/cache_hoodi_1265656.json @ 2693e018 ``` -That checksum is documentation of what the fixture should be, not an enforced -gate β€” the pinned commit is what makes the input immutable, and -`conversion_is_reproducible` pins the block's stats and serialized length. +That checksum is enforced, not just documented: `conversion_is_reproducible` +asserts it. The pinned commit keeps the *input* immutable; the digest keeps the +*derivation* honest. ## Getting a cache for a different block @@ -116,8 +116,9 @@ version-tolerant JSON instead of a pinned binary is the mitigation. ## Validation -Three checks, cheapest first. The first two run on the host in milliseconds and -need no RV64 toolchain. +Six checks, cheapest first. Five run on the host and need no RV64 toolchain; they +execute in milliseconds, though a cold build compiles the ethrex host dependency +tree, which is what costs ~60s in CI rather than the tests themselves. **`cargo test` here β€” `real_block_executes_under_guest_crypto`.** Executes the block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is @@ -141,19 +142,26 @@ can execute. Dropping `ethrex-config` would therefore be a CI-time improvement (it also sheds `ethrex-p2p`, `ethrex-blockchain`, `ethrex-storage` and the c-kzg and secp256k1 C builds), not a correctness fix. -**What screens 0x0a today** is `test_ethrex_real_block_native` in -`tooling/ethrex-tests`, whose graph links no KZG backend at all. That was -incidental to its dependencies, so `no_kzg_backend_linked` in the same file now -asserts it and will go red if anything pulls a backend in. +**`cargo test` here β€” `unmappable_network_is_rejected`.** Refuses a cache whose +`network` cannot be mapped to real chain rules rather than converting it under +substituted ones β€” see [above](#getting-a-cache-for-a-different-block) for why +that matters. **`cargo test` here β€” `conversion_is_reproducible`.** Pins the block's stats and -the serialized length, so an ethrex rev bump that moves the rkyv layout is -caught rather than silently producing a fixture the guest can't read. +the fixture's **sha256**. A length assert would not do: `ChainConfig` is +fixed-size, so a substituted chain config yields a byte-length-identical fixture, +and rkyv's `big_endian` feature would byte-swap in place β€” neither changes the +byte count. This is what catches an ethrex rev bump that moves the rkyv layout +instead of silently producing a fixture the guest can't read. + +**`tooling/ethrex-tests` β€” `no_kzg_backend_linked`.** Asserts that crate links no +KZG backend. That was incidental to its dependency graph, and it is the property +the next check relies on, so it is pinned here rather than assumed. **`tooling/ethrex-tests` β€” `test_ethrex_real_block_native`.** Checks the -serialized `.bin` itself deserializes and executes. That crate builds -`ethrex-guest-program` with default features, so this covers the artifact, not -the guest's precompile surface. +serialized `.bin` itself deserializes and executes. Since that crate links no KZG +backend, this is also **what screens point evaluation (0x0a)**: a block reaching +it diverges from consensus and fails here. **`tooling/ethrex-tests` β€” `test_ethrex_real_block_vm`** (`#[ignore]`, excluded from PR CI). The block through the guest ELF, comparing the VM's committed From 05e028512620dda72339963d59d048986ffae036 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 14:55:44 -0300 Subject: [PATCH 07/27] fix(tooling): re-download the ethrex-replay cache when its pinned rev changes The cache filename is keyed on the block number alone and its download rule had no prerequisites, so make treated an already-present `caches/cache_hoodi_.json` as up to date across an `ETHREX_REPLAY_REV` bump and kept converting the old input. Depending on a rev-stamped marker makes a re-pin discard the stale cache and refetch. Without this the mismatch only surfaced downstream as a `conversion_is_reproducible` digest failure whose message reads "regenerate the fixture", pointing at the wrong cause. CI never hit it (fresh checkout, `caches/` is gitignored); local re-pins did. --- Makefile | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dac63575f..aee6ef93c 100644 --- a/Makefile +++ b/Makefile @@ -286,10 +286,23 @@ ETHREX_REAL_BLOCK := 1265656 ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_hoodi_$(ETHREX_REAL_BLOCK).bin +# The cache filename is keyed on the block number only, and its download rule has +# no other prerequisite, so make would treat an already-present cache as up to +# date across an `ETHREX_REPLAY_REV` bump and silently keep converting the old +# input. Depending on a rev-stamped marker makes a re-pin discard the stale cache; +# without it the mismatch only surfaces downstream as a `conversion_is_reproducible` +# digest failure, which reads as "regenerate the fixture" and points at the wrong +# thing. CI never hits this (fresh checkout, `caches/` gitignored) β€” local re-pins do. +ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) -$(ETHREX_REAL_BLOCK_CACHE): +$(ETHREX_REPLAY_REV_STAMP): + mkdir -p $(dir $@) + rm -f $(ETHREX_REAL_BLOCK_CACHE) $(dir $@).replay-rev-* + touch $@ + +$(ETHREX_REAL_BLOCK_CACHE): $(ETHREX_REPLAY_REV_STAMP) mkdir -p $(dir $@) curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json From 50dfca460114f80803507001f8009407c34ea4e6 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 14:55:58 -0300 Subject: [PATCH 08/27] perf(ci): cache the detached ethrex workspaces and drop a duplicate build Two independent costs in the `test-executor` job, both paid on every PR run: - `Swatinem/rust-cache` defaulted to `. -> target`, so neither detached ethrex workspace was cached. `tooling/ethrex-real-block` alone locks ~335 packages, including the blst, c-kzg and secp256k1-sys C builds, malachite and ark-ff/asm. `cache-all-crates` covers the registry, not artifacts. Adds both tool workspaces to `workspaces:`. - k256's `expose-field` was requested only by `lambda-vm-ethrex-crypto`, a dev-dependency. Under resolver 3 dev-dependency features are not unified into builds that exclude them, so `cargo run` (the fixture step) and the `cargo test` right after it resolved different k256 feature sets and rebuilt k256 plus every ethrex crate above it. Moving that one dependency to `[dependencies]` makes both resolutions identical: cargo tree -e features,no-dev -i k256 # lists expose-field cargo tree -e features -i k256 # matches sha2 stays a dev-dependency; it has no feature delta either way. Also corrects the rkyv sync note: it named `executor/Cargo.toml`, which declares no rkyv. The readers are the guest and the two other detached ethrex tool workspaces. --- .github/workflows/pr_main.yaml | 10 ++++++++++ tooling/ethrex-real-block/Cargo.toml | 26 +++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index be4c4a5c6..0e71c2165 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -56,6 +56,16 @@ jobs: with: shared-key: "lambda-vm-test" cache-all-crates: "true" + # Both ethrex tool workspaces are detached (own Cargo.lock, own target + # dir), so the default `. -> target` misses them and their release + # trees rebuild from scratch on every run β€” ~335 packages for + # ethrex-real-block alone, including the blst, c-kzg and secp256k1-sys + # C builds, malachite and ark-ff/asm. `cache-all-crates` only covers + # the registry, not compiled artifacts. + workspaces: | + . -> target + tooling/ethrex-real-block -> target + tooling/ethrex-tests -> target - name: Cache compiled ASM ELF artifacts id: cache-asm-elfs diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-real-block/Cargo.toml index 2a37b4bcb..b56a5848c 100644 --- a/tooling/ethrex-real-block/Cargo.toml +++ b/tooling/ethrex-real-block/Cargo.toml @@ -24,16 +24,32 @@ ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156c ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } # Exact pin: the fixture writer and the guest/executor readers must agree on the -# rkyv layout. Keep this in sync with executor/{Cargo.toml,programs/rust/ethrex/Cargo.toml}. +# rkyv layout. Keep this in sync with the guest +# (executor/programs/rust/ethrex/Cargo.toml) and the other two detached ethrex +# tool workspaces (tooling/ethrex-fixtures, tooling/ethrex-tests). The `executor` +# crate itself declares no rkyv β€” the guest is what deserializes ProgramInput. rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -[dev-dependencies] -# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths -# both fall back to software, so this exercises the guest's *fallback* crypto -# semantics β€” not the accelerators themselves. +# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths both +# fall back to software, so this exercises the guest's *fallback* crypto semantics +# β€” not the accelerators themselves. +# +# Used only by the tests, but declared here rather than in `[dev-dependencies]` on +# purpose: it is the only thing that asks for k256's `expose-field`, and under +# resolver 3 a dev-dependency's features are not unified into builds that exclude +# dev-dependencies. As a dev-dep, `cargo run` (the `make ethrex-real-block-fixture` +# step) and the `cargo test` that follows it in CI resolve two different k256 +# feature sets, so k256 and every ethrex crate above it compile a second time in +# the same job. Confirm with: +# cargo tree -e features,no-dev -i k256 # must list expose-field +# cargo tree -e features -i k256 # must match the above lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } + +[dev-dependencies] +# No feature delta against the rest of the graph (sha2/std is already on), so this +# one can stay a dev-dependency without forcing the rebuild described above. sha2 = "0.10" # `ethrex-guest-program`'s `lambdavm` feature pins `lambda-vm-syscalls` to an From eea2c37bab64898c5659316e270bb42408768830 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 14:56:14 -0300 Subject: [PATCH 09/27] test(tooling): pin the real block's chain config independently of the digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `conversion_is_reproducible`'s sha256 is the assertion an ethrex rev bump forces someone to rewrite: the layout moves, the test goes red, a fresh digest gets pasted in. At that moment it stops covering the substituted- chain-config case the README picked it for. Assert the Hoodi chain id directly so that check survives the churn. Docs, all measured rather than estimated: - `ethrex_bench_20.bin` is 32,766 B, not 17 KB. The benchmark scripts generate it in `distinct` mode (20 senders to 20 recipients); 17 KB is the committed `ethrex_bench_4.bin`. `same` mode would be 16,811 B. Names the mode so the row is reproducible, and replaces "a handful" of trie nodes with the 40 accounts that mode actually touches. - "How to test" and the adopt-a-new-block steps omitted `make test-ethrex`, i.e. the only check that screens precompile 0x0a. This crate links a working c-kzg, so a block needing point evaluation passes here and fails in the guest β€” following the README alone would have missed it. - The validation list was labelled "cheapest first" but is ordered by the argument it builds, and the ~60s cold-build figure understated a ~335 package release tree. The per-test "milliseconds" claim is accurate (0.01-0.03s each) and is kept. Cache-absent failures now say to run `make ethrex-real-block-fixture` instead of surfacing a bare ENOENT from `unwrap()`. --- tooling/ethrex-real-block/README.md | 29 ++++++++++++++++++++++----- tooling/ethrex-real-block/src/main.rs | 23 ++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index c0e5fcf37..aaed79ae1 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -15,13 +15,19 @@ opposite β€” a block that actually looks like Ethereum. | transactions | 20 (all plain transfers) | 11 (7Γ— EIP-1559, 4Γ— EIP-4844 blob) | | contract calls | 0 | 5, at ~830–955k gas each | | contract bytecode in witness | 0 | 22 contracts, ~124 KB | -| state-trie nodes | a handful | 1,705 | +| state trie | 40 accounts, fresh genesis | 1,705 nodes | | storage keys | 0 | 422 | -| serialized size | 17 KB | 1,021,207 B | +| serialized size | 32,766 B | 1,021,207 B | Note it has *fewer* transactions than the synthetic fixture while being far more representative β€” transaction count is not the axis that matters. +The synthetic column is `ethrex_bench_20.bin` as the benchmark scripts actually +generate it β€” `ethrex-fixtures 20 … distinct`, i.e. 20 distinct genesis-funded +senders to 20 distinct recipients (`scripts/bench_verify.sh`, +`scripts/bench_recursion_scaling.sh`). The same block in `same` mode (one sender, +one recipient) serializes to 16,811 B. + Block 1265656 is **verified to run on the guest's precompile surface** (see [Validation](#validation)); it needs no accelerator we don't have. Any replacement block must clear the same check β€” that is what makes it usable, not @@ -96,6 +102,12 @@ default is an edit, not a flag: `ETHREX_REAL_BLOCK` and `ETHREX_REPLAY_REV` in t Makefile, the `hoodi` in the cache URL, `CACHE` and the assertions in `src/main.rs`, and `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests`. +Then run **`make test-ethrex`**, not just this crate's tests. A new block is only +usable if it needs no accelerator the guest lacks, and this crate cannot tell you +that β€” its graph links a working c-kzg, so a block calling point evaluation (0x0a) +passes here and fails in the guest. `test_ethrex_real_block_native` in +`tooling/ethrex-tests` is the screen. See [Validation](#validation). + ## Why the JSON and not ethrex-replay's own `.bin` `ethrex-replay` can already emit a rkyv `ProgramInput`, but it tracks ethrex @@ -116,9 +128,16 @@ version-tolerant JSON instead of a pinned binary is the mitigation. ## Validation -Six checks, cheapest first. Five run on the host and need no RV64 toolchain; they -execute in milliseconds, though a cold build compiles the ethrex host dependency -tree, which is what costs ~60s in CI rather than the tests themselves. +Six checks, ordered so the argument builds: the host-side parity test first, then +what it does *not* cover, then the checks in `tooling/ethrex-tests` that close the +gap. Five run on the host and need no RV64 toolchain, and each executes in +milliseconds. + +What costs time is a cold build of the ethrex host dependency tree β€” ~335 +packages, including the `blst`, `c-kzg` and `secp256k1-sys` C builds, `malachite` +and `ark-ff/asm` β€” not the tests. CI caches this workspace's `target/` (see the +`workspaces:` list on the `test-executor` job's `rust-cache` step), so only cold +runs pay it. **`cargo test` here β€” `real_block_executes_under_guest_crypto`.** Executes the block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index 72ccee3f6..f63ef97c5 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -113,9 +113,16 @@ fn main() -> Result<(), Box> { #[cfg(test)] mod tests { use super::*; + use ethrex_config::networks::HOODI_CHAIN_ID; const CACHE: &str = "caches/cache_hoodi_1265656.json"; + /// `caches/` is gitignored and fetched on demand, so every test here fails on + /// a clean checkout until the cache is downloaded. Say so instead of surfacing + /// a bare `No such file or directory` from `unwrap()`. + const CACHE_MISSING: &str = "caches/cache_hoodi_1265656.json is missing β€” run \ + `make ethrex-real-block-fixture` from the repo root first"; + /// Executes the converted block with `LambdaVmEcsmCrypto`, the `Crypto` impl /// the guest injects, so the block is exercised through the same trait /// dispatch the guest uses. Stateless re-execution ends in a post-state-root @@ -138,7 +145,7 @@ mod tests { use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; use std::sync::Arc; - let (program_input, _) = program_input_from_cache(CACHE).unwrap(); + let (program_input, _) = program_input_from_cache(CACHE).expect(CACHE_MISSING); execution_program(program_input, Arc::new(LambdaVmEcsmCrypto)).unwrap(); } @@ -149,7 +156,7 @@ mod tests { #[test] fn unmappable_network_is_rejected() { let mut cache: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(CACHE).unwrap()).unwrap(); + serde_json::from_str(&std::fs::read_to_string(CACHE).expect(CACHE_MISSING)).unwrap(); cache["network"] = serde_json::json!("LocalDevnet"); let path = std::env::temp_dir().join(format!( @@ -176,13 +183,23 @@ mod tests { fn conversion_is_reproducible() { use sha2::Digest; - let (program_input, summary) = program_input_from_cache(CACHE).unwrap(); + let (program_input, summary) = program_input_from_cache(CACHE).expect(CACHE_MISSING); let bytes = rkyv::to_bytes::(&program_input).unwrap(); assert_eq!(summary.first_block_number, 1_265_656); assert_eq!(summary.transactions, 11); assert_eq!(summary.gas_used, 4_402_947); + // Asserted separately from the digest below, not covered by it. The digest + // is exactly what a legitimate ethrex rev bump forces someone to rewrite + // (the layout moves, this goes red, a fresh digest gets pasted in) β€” and at + // that moment it stops covering the substituted-chain-config case it was + // chosen for. This assert survives that churn. + assert_eq!( + program_input.execution_witness.chain_config.chain_id, HOODI_CHAIN_ID, + "chain config is not Hoodi's β€” the block would replay under other rules", + ); + // Digest, not length: a layout change can preserve the byte count exactly // (`ChainConfig` is fixed-size, and rkyv's `big_endian` feature would only // byte-swap in place), so `len()` cannot pin the archived form. From da9a82ba4da58ddb5575b07aec77c986dcdd0900 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 16:43:48 -0300 Subject: [PATCH 10/27] bench(ci): make a real Ethereum block the headline benchmark workload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real-block fixture from tooling/ethrex-real-block existed but nothing benchmarked with it: every perf measurement still ran on ethrex_bench_20, a synthetic block of 20 plain transfers. Measured on the same guest ELF, the two workloads are not the same benchmark: ethrex_bench_20 14.2M cycles 411 keccak 80 ecsm real block 168.3M cycles 9,046 keccak 44 ecsm so the synthetic block is ecrecover-bound where a real one is keccak- and trie-bound, and a prover change can move the two in opposite directions. Wire the real block in as the headline, gated exactly like the growth sweep β€” `/bench-real`, push to main, workflow_dispatch β€” and NOT on plain `/bench`. One real-block prove is ~15 min on the CPU bench runner against ~25s for the synthetic block, and that runner is single and serialized across all PRs. Because push-to-main always publishes a real-block number into the baseline artifact, `/bench-real` on a PR pays for its own side only. The synthetic block stays as the fast per-comment screen, relabelled as such; GROWTH_PROGRAMS stays synthetic because it plots heap against block size, which needs a family of blocks and cannot come from one real one. Continuations are forced for the real block rather than chosen: monolithic peak heap grows ~3.1 GB per million cycles on this workload family (2007 MB/transfer at R^2 = 0.998, from the current main baseline artifact), so 168M cycles would need ~500 GB. At epoch 2^21 it fits in ~16 GB. Supporting changes: - cli: report `Peak heap` from `prove --continuations`. The tracker only existed on the monolithic path, so the continuation path had no heap metric at all and the benchmark's parser would have failed on it. - Makefile: derive every real-block path from ETHREX_REAL_BLOCK_NETWORK + ETHREX_REAL_BLOCK, and expose `print-real-block-fixture`. Repointing the block is now those two lines; scripts and CI resolve the path from make instead of naming the fixture themselves. - benchmark-pr.yml: anchor the baseline-artifact greps. `real_time_s` contains `time_s`, so the existing unanchored greps would have matched two lines and written a multi-line step output. - benchmark-pr.yml: record which block was measured and refuse to diff against a baseline that measured a different one, so a fixture swap cannot masquerade as a regression. - bench_verify.sh / perf_diff.sh: WORKLOAD=synthetic|real, defaulting to synthetic. Both auto-generate the gitignored fixture when absent, and bench_verify.sh carries the prove flags in its proof-cache marker so a WORKLOAD switch cannot reuse the other workload's proof. - executor/.gitignore: ignore every accepted network's fixture name, not just hoodi's, so repointing cannot make a ~1 MB fixture committable. --- .github/workflows/benchmark-pr.yml | 320 ++++++++++++++++++++++++++-- Makefile | 26 ++- bin/cli/src/main.rs | 11 + executor/.gitignore | 5 + scripts/bench_verify.sh | 93 ++++++-- scripts/perf_diff.sh | 47 +++- tooling/ethrex-real-block/README.md | 57 ++++- 7 files changed, 517 insertions(+), 42 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index fd74c7b3f..5229ce517 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -12,6 +12,13 @@ on: - 'executor/**' - 'bin/cli/**' - 'tooling/ethrex-fixtures/**' + # A baseline is only valid for the workload it measured, and these two + # define the real-block workload: the converter decides the fixture's + # bytes, the Makefile decides which block it is. Without them a repointed + # block would leave main's baseline stale until some prover file happened + # to change β€” and the comparison guard would suppress the table until then. + - 'tooling/ethrex-real-block/**' + - 'Makefile' # Uncomment to auto-run on PRs: # pull_request: # branches: [main] @@ -32,12 +39,36 @@ concurrency: cancel-in-progress: false env: - # Headline program: the ethrex guest ELF proven against a 20-transfer block + # Cheap screen: the ethrex guest ELF proven against a SYNTHETIC 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). + # + # This is not a representative block β€” 20 plain transfers are ecrecover-heavy and + # touch almost no state (measured, same ELF: 14.2M cycles, 411 keccak calls, 80 + # ecsm calls). It stays the per-comment default only because it proves in ~25s; + # the representative number is REAL_BLOCK below, which runs on push/dispatch and + # on an explicit `/bench-real`. ELF: executor/program_artifacts/rust/ethrex.elf INPUT: executor/tests/ethrex_bench_20.bin + # Headline (representative) workload: a real Ethereum block, converted from an + # ethrex-replay cache by tooling/ethrex-real-block. Same measured ELF: 168.3M + # cycles, 9,046 keccak calls, 44 ecsm calls β€” i.e. ~12x the work of the synthetic + # block with a ~40x different keccak:ecrecover mix. That is the whole point: a + # prover change can move the synthetic number and the real one in opposite + # directions. + # + # The path is NOT hardcoded here: it is resolved from the Makefile + # (`make -s print-real-block-fixture`) into REAL_INPUT at run time, so repointing + # the block is a Makefile edit and nothing else. The .bin is ~1 MB and gitignored, + # so the job generates it (see "Generate ethrex real-block fixture"). + # + # Continuations are mandatory here, not a preference: a monolithic prove costs + # ~3.1 GB of peak heap per million cycles on this workload family (measured + # growth slope, R^2 = 0.998), so 168M cycles would need ~500 GB. `--continuations` + # makes peak heap flat in the epoch size instead of the trace length. + REAL_BLOCK_EPOCH_LOG2: "21" + BENCH_RUNS_REAL: 1 BENCH_RUNS_PR: 3 # Cheap-tier screen: catches regressions down to ~1.5% on its own and leaves # smaller/ambiguous deltas to the manual drift-free ABBA tiebreaker. Pushing either @@ -53,7 +84,10 @@ env: jobs: benchmark: runs-on: [self-hosted, bench] - # Skip unless: push to main, workflow_dispatch, or "/bench" comment on a PR + # Skip unless: push to main, workflow_dispatch, or "/bench" comment on a PR. + # "/bench-real" and "/bench-growth" are handled by THIS job (they are prefixed + # by "/bench" and deliberately absent from the exclusion list below); they only + # switch which sections run, in the "Determine run count" step. if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || @@ -135,6 +169,29 @@ jobs: echo "run_growth=false" >> "$GITHUB_OUTPUT" fi + # Real-block benchmark: same gating as growth (/bench-real, push to main, + # workflow_dispatch), and for the same reason β€” it is far too slow for the + # per-comment tier. One continuation prove of the real block is ~15 min of + # CPU on this runner (extrapolated from the ~7 min cont100 figure in + # bench-abba.yml at ~2.5x the cycles), against ~25s for the synthetic + # block, and the runner is single and serialized across all PRs. + # + # Because push-to-main always runs it, the baseline artifact carries a + # real-block number, so `/bench-real` on a PR pays for the PR side only. + if [ "$EVENT_NAME" = "issue_comment" ] && echo "$COMMENT_BODY" | grep -q '^/bench-real'; then + echo "run_real=true" >> "$GITHUB_OUTPUT" + elif [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "run_real=true" >> "$GITHUB_OUTPUT" + else + echo "run_real=false" >> "$GITHUB_OUTPUT" + fi + + # Resolve the real-block fixture path from its single source of truth (the + # Makefile) and pin it in the job env. Captured HERE, before the baseline + # step's `git checkout origin/main`, so the PR and main sides agree on the + # workload even when main's Makefile names a different block. + echo "REAL_INPUT=$(make -s print-real-block-fixture)" >> "$GITHUB_ENV" + if [ "$EVENT_NAME" = "issue_comment" ]; then CUSTOM_N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench[[:space:]]*\([0-9]\+\).*|\1|p') RUNS=${CUSTOM_N:-$BENCH_RUNS_PR} @@ -169,6 +226,19 @@ jobs: echo "Using $RUNS iterations, TABLE_PARALLELISM=default" fi + - name: Generate ethrex real-block fixture + if: steps.config.outputs.run_real == 'true' + run: | + # ~1 MB and gitignored, so it is never in the checkout: build it rather + # than failing on a missing file, the same way the synthetic fixtures are + # generated above. Cold runs also fetch a ~1.5 MB ethrex-replay cache over + # the network (pinned to an immutable rev, so the input can't drift). + # + # Untracked, so like the ELF it survives `git checkout origin/main` and both + # sides prove the identical block. + make ethrex-real-block-fixture + ls -l "$REAL_INPUT" + - name: Benchmark PR id: pr env: @@ -312,6 +382,60 @@ jobs: echo "growth_slope_mb=$SLOPE" >> /tmp/metrics.txt echo "growth_r2=$R2" >> /tmp/metrics.txt + - name: Real block (PR) + id: pr-real + if: steps.config.outputs.run_real == 'true' + env: + RUNS: ${{ env.BENCH_RUNS_REAL }} + TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} + run: | + if [ -n "$TABLE_PARALLELISM" ]; then + export TABLE_PARALLELISM + echo "TABLE_PARALLELISM=$TABLE_PARALLELISM" + fi + TIMES="" + HEAPS="" + EPOCHS="" + for i in $(seq 1 "$RUNS"); do + echo "--- Real block run $i/$RUNS (continuations, epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" + ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \ + --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \ + -o /tmp/real_proof.bin --time | tee /tmp/real_output_$i.txt + rm -f /tmp/real_proof.bin + + T=$(grep -o 'Proving time: [0-9.]*' /tmp/real_output_$i.txt | awk '{print $3}') + # Peak heap is optional here, unlike the monolithic step: the continuation + # prove path only learned to report it alongside this benchmark, so a + # baseline built from an older main prints no such line. Missing heap + # degrades one table cell; a missing time makes the run meaningless. + H=$(grep -o 'Peak heap: [0-9]*' /tmp/real_output_$i.txt | awk '{print $3}') + E=$(grep -o 'Epochs: [0-9]*' /tmp/real_output_$i.txt | awk '{print $2}') + + if [ -z "$T" ]; then + echo "::error::Failed to parse real-block proving time from run $i" + cat /tmp/real_output_$i.txt + exit 1 + fi + if [ -z "$H" ]; then + echo "::warning::No 'Peak heap' line on the real-block run (CLI predates continuation heap reporting)" + fi + + TIMES="$TIMES $T" + if [ -n "$H" ]; then HEAPS="$HEAPS $H"; fi + EPOCHS="$E" + done + + MEDIAN_POS=$(( (RUNS + 1) / 2 )) + TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") + HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") + + { + echo "real_time_s=$TIME_MEDIAN" + echo "real_peak_mb=$HEAP_MEDIAN" + echo "real_epochs=$EPOCHS" + echo "real_input=$(basename "$REAL_INPUT")" + } | tee -a /tmp/metrics.txt >> "$GITHUB_OUTPUT" + - name: Upload metrics artifact uses: actions/upload-artifact@v4 with: @@ -338,17 +462,17 @@ jobs: BASELINE_FILE=$(ls -t baseline/*/metrics.txt 2>/dev/null | head -1) if [ -n "$BASELINE_FILE" ]; then echo "found=true" >> "$GITHUB_OUTPUT" - echo "peak_mb=$(grep 'peak_mb=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "time_s=$(grep 'time_s=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "time_spread=$(grep 'time_spread=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "heap_spread=$(grep 'heap_spread=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "all_times=$(grep 'all_times=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "all_heaps=$(grep 'all_heaps=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "runs=$(grep 'runs=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "growth_heaps=$(grep 'growth_heaps=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "growth_times=$(grep 'growth_times=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "growth_slope_mb=$(grep 'growth_slope_mb=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "growth_r2=$(grep 'growth_r2=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT" + # Anchored (`^key=`): the real-block keys added below are suffixes of + # the plain ones (`real_time_s` contains `time_s`), so an unanchored + # grep would return TWO lines and write a multi-line step output. + get() { grep "^$1=" "$BASELINE_FILE" | head -1 | cut -d= -f2; } + for key in peak_mb time_s time_spread heap_spread all_times all_heaps \ + runs growth_heaps growth_times growth_slope_mb growth_r2 \ + real_time_s real_peak_mb real_input; do + # Real-block keys are absent from baselines produced before the + # real-block section existed; empty values just hide that table. + echo "$key=$(get "$key")" >> "$GITHUB_OUTPUT" + done exit 0 fi fi @@ -363,6 +487,7 @@ jobs: GH_TOKEN: ${{ github.token }} RUNS: ${{ steps.config.outputs.runs }} RUN_GROWTH: ${{ steps.config.outputs.run_growth }} + RUN_REAL: ${{ steps.config.outputs.run_real }} TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} run: | if [ -n "$TABLE_PARALLELISM" ]; then @@ -495,6 +620,32 @@ jobs: echo "growth_r2=$R2" >> "$GITHUB_OUTPUT" fi # end run_growth check + # --- Real block (continuations, 1 sample) --- + # Only reached when no cached baseline exists, which is the expensive case: + # push-to-main normally publishes the real-block numbers, so a /bench-real + # comment pays for the PR side alone. $REAL_INPUT was resolved before this + # checkout and the fixture is untracked, so main proves the identical block. + if [ "$RUN_REAL" != "true" ]; then + echo "Skipping real-block baseline (use /bench-real to enable)" + else + echo "--- Baseline real block (continuations, epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" + ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \ + --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \ + -o /tmp/real_proof.bin --time | tee /tmp/baseline_real.txt + rm -f /tmp/real_proof.bin + T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_real.txt | awk '{print $3}') + # Optional, as on the PR side: a main that predates continuation heap + # reporting prints no such line, and that must not fail the comparison. + H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_real.txt | awk '{print $3}') + if [ -z "$T" ]; then + echo "::error::Failed to parse baseline real-block proving time" + cat /tmp/baseline_real.txt + exit 1 + fi + echo "real_time_s=$T" >> "$GITHUB_OUTPUT" + echo "real_peak_mb=$H" >> "$GITHUB_OUTPUT" + fi + # Restore PR checkout git checkout "$PR_SHA" @@ -517,6 +668,9 @@ jobs: BA_GROWTH_TIMES: ${{ steps.baseline-artifact.outputs.growth_times }} BA_GROWTH_SLOPE: ${{ steps.baseline-artifact.outputs.growth_slope_mb }} BA_GROWTH_R2: ${{ steps.baseline-artifact.outputs.growth_r2 }} + BA_REAL_TIME: ${{ steps.baseline-artifact.outputs.real_time_s }} + BA_REAL_PEAK: ${{ steps.baseline-artifact.outputs.real_peak_mb }} + BA_REAL_INPUT: ${{ steps.baseline-artifact.outputs.real_input }} # Baseline run outputs BR_PEAK_MB: ${{ steps.baseline-run.outputs.peak_mb }} BR_TIME_S: ${{ steps.baseline-run.outputs.time_s }} @@ -529,6 +683,8 @@ jobs: BR_GROWTH_TIMES: ${{ steps.baseline-run.outputs.growth_times }} BR_GROWTH_SLOPE: ${{ steps.baseline-run.outputs.growth_slope_mb }} BR_GROWTH_R2: ${{ steps.baseline-run.outputs.growth_r2 }} + BR_REAL_TIME: ${{ steps.baseline-run.outputs.real_time_s }} + BR_REAL_PEAK: ${{ steps.baseline-run.outputs.real_peak_mb }} # PR outputs CURRENT_PEAK: ${{ steps.pr.outputs.peak_mb }} CURRENT_TIME: ${{ steps.pr.outputs.time_s }} @@ -542,6 +698,11 @@ jobs: PR_GROWTH_TIMES: ${{ steps.pr-growth.outputs.growth_times }} PR_GROWTH_SLOPE: ${{ steps.pr-growth.outputs.growth_slope_mb }} PR_GROWTH_R2: ${{ steps.pr-growth.outputs.growth_r2 }} + # PR real-block outputs + PR_REAL_TIME: ${{ steps.pr-real.outputs.real_time_s }} + PR_REAL_PEAK: ${{ steps.pr-real.outputs.real_peak_mb }} + PR_REAL_EPOCHS: ${{ steps.pr-real.outputs.real_epochs }} + PR_REAL_INPUT: ${{ steps.pr-real.outputs.real_input }} run: | # Pick baseline source if [ "$BASELINE_FOUND" = "true" ]; then @@ -557,6 +718,9 @@ jobs: BASELINE_GROWTH_TIMES="$BA_GROWTH_TIMES" BASELINE_GROWTH_SLOPE="$BA_GROWTH_SLOPE" BASELINE_GROWTH_R2="$BA_GROWTH_R2" + BASELINE_REAL_TIME="$BA_REAL_TIME" + BASELINE_REAL_PEAK="$BA_REAL_PEAK" + BASELINE_REAL_INPUT="$BA_REAL_INPUT" else BASELINE_PEAK="$BR_PEAK_MB" BASELINE_TIME="$BR_TIME_S" @@ -570,6 +734,11 @@ jobs: BASELINE_GROWTH_TIMES="$BR_GROWTH_TIMES" BASELINE_GROWTH_SLOPE="$BR_GROWTH_SLOPE" BASELINE_GROWTH_R2="$BR_GROWTH_R2" + BASELINE_REAL_TIME="$BR_REAL_TIME" + BASELINE_REAL_PEAK="$BR_REAL_PEAK" + # Freshly proven on this runner from $REAL_INPUT, so by construction the + # same block the PR side used; the cached path carries its own label. + BASELINE_REAL_INPUT="$PR_REAL_INPUT" fi if [ -z "$BASELINE_PEAK" ] || [ "$BASELINE_PEAK" -eq 0 ] 2>/dev/null || @@ -611,6 +780,36 @@ jobs: echo "baseline_growth_slope=$BASELINE_GROWTH_SLOPE" >> "$GITHUB_OUTPUT" echo "baseline_growth_r2=$BASELINE_GROWTH_R2" >> "$GITHUB_OUTPUT" + # Real-block comparison. Rendered only when BOTH sides have a number AND + # they are the same block: a baseline captured before the Makefile was + # repointed measures a different workload, and showing that as a delta + # would invent a regression out of a fixture swap. + echo "pr_real_time=$PR_REAL_TIME" >> "$GITHUB_OUTPUT" + echo "pr_real_peak=$PR_REAL_PEAK" >> "$GITHUB_OUTPUT" + echo "pr_real_epochs=$PR_REAL_EPOCHS" >> "$GITHUB_OUTPUT" + echo "pr_real_input=$PR_REAL_INPUT" >> "$GITHUB_OUTPUT" + echo "baseline_real_time=$BASELINE_REAL_TIME" >> "$GITHUB_OUTPUT" + echo "baseline_real_peak=$BASELINE_REAL_PEAK" >> "$GITHUB_OUTPUT" + echo "baseline_real_input=$BASELINE_REAL_INPUT" >> "$GITHUB_OUTPUT" + + if [ -n "$PR_REAL_TIME" ] && [ -n "$BASELINE_REAL_TIME" ]; then + if [ -n "$BASELINE_REAL_INPUT" ] && [ "$BASELINE_REAL_INPUT" != "$PR_REAL_INPUT" ]; then + echo "::warning::Baseline real block ($BASELINE_REAL_INPUT) differs from the PR's ($PR_REAL_INPUT); not comparing." + echo "real_mismatch=$BASELINE_REAL_INPUT" >> "$GITHUB_OUTPUT" + else + REAL_TIME_DIFF=$(awk "BEGIN { printf \"%.3f\", $PR_REAL_TIME - $BASELINE_REAL_TIME }") + REAL_TIME_PCT=$(awk "BEGIN { printf \"%.1f\", (($PR_REAL_TIME - $BASELINE_REAL_TIME) * 100) / $BASELINE_REAL_TIME }") + echo "real_time_diff=$REAL_TIME_DIFF" >> "$GITHUB_OUTPUT" + echo "real_time_pct=$REAL_TIME_PCT" >> "$GITHUB_OUTPUT" + if [ -n "$PR_REAL_PEAK" ] && [ -n "$BASELINE_REAL_PEAK" ]; then + REAL_PEAK_DIFF=$((PR_REAL_PEAK - BASELINE_REAL_PEAK)) + REAL_PEAK_PCT=$(awk "BEGIN { printf \"%.1f\", ($REAL_PEAK_DIFF * 100) / $BASELINE_REAL_PEAK }") + echo "real_peak_diff=$REAL_PEAK_DIFF" >> "$GITHUB_OUTPUT" + echo "real_peak_pct=$REAL_PEAK_PCT" >> "$GITHUB_OUTPUT" + fi + fi + fi + # Growth slope comparison if [ -n "$BASELINE_GROWTH_SLOPE" ] && [ -n "$PR_GROWTH_SLOPE" ]; then SLOPE_DIFF=$((PR_GROWTH_SLOPE - BASELINE_GROWTH_SLOPE)) @@ -651,6 +850,18 @@ jobs: BASE_GROWTH_R2: ${{ steps.compare.outputs.baseline_growth_r2 }} GROWTH_SLOPE_DIFF: ${{ steps.compare.outputs.growth_slope_diff }} GROWTH_SLOPE_PCT: ${{ steps.compare.outputs.growth_slope_pct }} + PR_REAL_TIME: ${{ steps.compare.outputs.pr_real_time }} + PR_REAL_PEAK: ${{ steps.compare.outputs.pr_real_peak }} + PR_REAL_EPOCHS: ${{ steps.compare.outputs.pr_real_epochs }} + PR_REAL_INPUT: ${{ steps.compare.outputs.pr_real_input }} + BASE_REAL_TIME: ${{ steps.compare.outputs.baseline_real_time }} + BASE_REAL_PEAK: ${{ steps.compare.outputs.baseline_real_peak }} + REAL_TIME_DIFF: ${{ steps.compare.outputs.real_time_diff }} + REAL_TIME_PCT: ${{ steps.compare.outputs.real_time_pct }} + REAL_PEAK_DIFF: ${{ steps.compare.outputs.real_peak_diff }} + REAL_PEAK_PCT: ${{ steps.compare.outputs.real_peak_pct }} + REAL_MISMATCH: ${{ steps.compare.outputs.real_mismatch }} + REAL_EPOCH_LOG2: ${{ env.REAL_BLOCK_EPOCH_LOG2 }} COMMIT_SHA: ${{ steps.pr-ref.outputs.sha || github.sha }} TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} with: @@ -690,12 +901,76 @@ jobs: const icon = (pct) => parseFloat(pct) > 5 ? 'πŸ”΄' : parseFloat(pct) < -5 ? '🟒' : 'βšͺ'; const SPREAD_THRESHOLD = 5.0; - // --- Section 1: Primary benchmark --- + // Real-block data + const realTime = process.env.PR_REAL_TIME; + const realPeak = process.env.PR_REAL_PEAK; + const realEpochs = process.env.PR_REAL_EPOCHS; + const realInput = process.env.PR_REAL_INPUT; + const baseRealTime = process.env.BASE_REAL_TIME; + const baseRealPeak = process.env.BASE_REAL_PEAK; + const realTimeDiff = process.env.REAL_TIME_DIFF; + const realTimePct = process.env.REAL_TIME_PCT; + const realPeakDiff = process.env.REAL_PEAK_DIFF; + const realPeakPct = process.env.REAL_PEAK_PCT; + const realMismatch = process.env.REAL_MISMATCH; + const realEpochLog2 = process.env.REAL_EPOCH_LOG2; + + // Stable marker: the "find and update the existing comment" lookup at the + // bottom keys off it, so section headings can change without orphaning + // every comment already posted. Legacy title matches stay as a fallback. + let body = `\n`; + + // --- Section 1: Real block (headline; only present on /bench-real, push, dispatch) --- + if (realTime) { + body += `## Benchmark β€” real block${realInput ? ` (\`${realInput}\`)` : ''}\n\n`; + body += `continuations Β· epoch 2^${realEpochLog2}`; + if (realEpochs) body += ` Β· ${realEpochs} epochs`; + body += ` Β· 1 sample\n\n`; + + if (realMismatch) { + body += `> ⚠️ Baseline measured a different block (\`${realMismatch}\`) β€” showing the PR side only.\n\n`; + } + + const haveRealCmp = !!(baseRealTime && realTimePct && !realMismatch); + if (haveRealCmp) { + body += `| Metric | main | PR | Ξ” |\n`; + body += `|--------|------|----|---|\n`; + if (realPeak && baseRealPeak && realPeakPct) { + body += `| **Peak heap** | ${baseRealPeak} MB | ${realPeak} MB | ${fmt(realPeakDiff)} MB (${fmt(realPeakPct)}%) ${icon(realPeakPct)} |\n`; + } + body += `| **Prove time** | ${baseRealTime}s | ${realTime}s | ${fmt(realTimeDiff)}s (${fmt(realTimePct)}%) ${icon(realTimePct)} |\n\n`; + + // 1 sample, so the >5% band that gates the synthetic screen is not + // supportable here: call out only what is far outside run-to-run noise, + // and say plainly that the middle band is unresolved rather than "fine". + const rp = parseFloat(realTimePct); + if (rp > 10) { + body += `> ⚠️ **Regression on the real block** β€” prove time up ${fmt(realTimePct)}%.\n`; + } else if (rp < -10) { + body += `> πŸŽ‰ **Improvement on the real block** β€” prove time down ${realTimePct}%.\n`; + } else if (Math.abs(rp) >= 3) { + body += `> ❓ **${fmt(realTimePct)}% β€” not resolved by one sample.** Re-run \`/bench-real\`, or use \`/bench-abba\` for a paired test.\n`; + } else { + body += `> βœ… No significant change.\n`; + } + } else { + body += `| Metric | PR |\n`; + body += `|--------|----|\n`; + if (realPeak) body += `| **Peak heap** | ${realPeak} MB |\n`; + body += `| **Prove time** | ${realTime}s |\n\n`; + if (!realMismatch) { + body += `> ℹ️ No real-block baseline yet β€” main publishes one on its next push.\n`; + } + } + body += `\n`; + } + + // --- Section 2: Synthetic screen --- const nLabel = parseInt(runs) > 1 ? ` (median of ${runs})` : ''; const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; - let body = `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Table parallelism: ${tpLabel}\n\n`; + body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; + body += `Synthetic screen β€” 20 plain transfers, ~12x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Ξ” |\n`; body += `|--------|------|----|---|\n`; body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; @@ -757,7 +1032,10 @@ jobs: body += `\n> βœ… Low variance (time: ${prTimeSpread || '0.0'}%, heap: ${prHeapSpread || '0.0'}%)\n`; } - // --- Section 2: Memory growth --- + // --- Section 3: Memory growth --- + // Synthetic on purpose: this plots heap against BLOCK SIZE, which needs a + // family of blocks that differ only in transaction count. A real block is + // one point and cannot produce a slope. if (prGrowthHeaps) { const prHeaps = prGrowthHeaps.split('/'); const baseHeaps = baseGrowthHeaps ? baseGrowthHeaps.split('/') : null; @@ -816,6 +1094,10 @@ jobs: } // --- Footer --- + if (!realTime) { + body += `\n> 🧱 **This ran on synthetic blocks only.** Comment \`/bench-real\` to prove a real Ethereum block `; + body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~15 min to the run.\n`; + } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; @@ -825,9 +1107,11 @@ jobs: issue_number: context.issue.number, }); - // Find existing comment (check both old and new markers for transition) + // Find existing comment. The HTML marker is the durable key; the title + // matches below are the transition path for comments posted before it. const existing = comments.find(c => c.user.type === 'Bot' && ( + c.body.includes('') || c.body.includes('Benchmark β€” ethrex') || c.body.includes('Benchmark β€” fib_iterative_8M') || c.body.includes('Benchmark β€” fib_iterative_2M') || diff --git a/Makefile b/Makefile index aee6ef93c..be6e2c8bc 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ test-profile-recursion-block recursion-profile-block-input \ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \ test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ -update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture +update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \ +print-real-block-fixture UNAME := $(shell uname) @@ -279,13 +280,23 @@ test-rust: compile-programs-rust # The source is ethrex-replay's cache JSON, not that tool's own rkyv output: # ethrex-replay tracks ethrex `main`, where `ProgramInput` has diverged from the # rev our guest pins, so only the JSON is safe to read across the two. +# +# Which block the benchmarks run is these TWO lines and nothing else: every path +# below (cache file, download URL, fixture name) is derived from them, and the +# benchmark scripts / CI resolve the fixture through `make -s print-real-block-fixture` +# rather than spelling it out. Repointing to, say, mainnet 25453112 is +# `ETHREX_REAL_BLOCK_NETWORK := mainnet` + the new number + a matching +# `ETHREX_REPLAY_REV`, then the test-side pins listed in +# tooling/ethrex-real-block/README.md ("Getting a cache for a different block"). +ETHREX_REAL_BLOCK_NETWORK := hoodi ETHREX_REAL_BLOCK := 1265656 # Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would # let the benchmark's input change under a fixed fixture name, silently breaking # comparability across runs. Re-pin deliberately when adopting a new block. ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 -ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json -ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_hoodi_$(ETHREX_REAL_BLOCK).bin +ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) +ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_REAL_BLOCK_ID).json +ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin # The cache filename is keyed on the block number only, and its download rule has # no other prerequisite, so make would treat an already-present cache as up to # date across an `ETHREX_REPLAY_REV` bump and silently keep converting the old @@ -297,6 +308,13 @@ ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) +# Single source of truth for the benchmark tooling. scripts/bench_verify.sh, +# scripts/perf_diff.sh and .github/workflows/benchmark-pr.yml read the fixture +# path from here instead of hardcoding it, so repointing the block above moves +# every benchmark at once. `-s` on the caller's side keeps the output clean. +print-real-block-fixture: + @echo $(ETHREX_REAL_BLOCK_FIXTURE) + $(ETHREX_REPLAY_REV_STAMP): mkdir -p $(dir $@) rm -f $(ETHREX_REAL_BLOCK_CACHE) $(dir $@).replay-rev-* @@ -305,7 +323,7 @@ $(ETHREX_REPLAY_REV_STAMP): $(ETHREX_REAL_BLOCK_CACHE): $(ETHREX_REPLAY_REV_STAMP) mkdir -p $(dir $@) curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ - https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_hoodi_$(ETHREX_REAL_BLOCK).json + https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_REAL_BLOCK_ID).json mv $@.tmp $@ $(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-block/src/main.rs tooling/ethrex-real-block/Cargo.toml tooling/ethrex-real-block/Cargo.lock diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 1de36220b..a04e920db 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -790,6 +790,12 @@ fn cmd_prove_continuation( eprintln!( "Generating continuation proof (blowup={blowup}, epoch_size_log2={epoch_size_log2}, epoch_size={epoch_size})...", ); + // Same tracker as the monolithic path: the peak here is the flat per-epoch + // working set rather than a whole-trace high-water mark, and it is the metric + // that decides which epoch size a machine can run, so the benchmarks need it + // reported identically on both paths. + #[cfg(feature = "jemalloc-stats")] + let tracker = heap_tracker::HeapTracker::start(); let start = Instant::now(); let bundle = match prover::continuation::prove_continuation( &elf_data, @@ -834,6 +840,11 @@ fn cmd_prove_continuation( if time { println!("Proving time: {:.3}s", prove_elapsed.as_secs_f64()); } + #[cfg(feature = "jemalloc-stats")] + { + let peak_bytes = tracker.stop(); + println!("Peak heap: {} MB", peak_bytes / (1024 * 1024)); + } ExitCode::SUCCESS } diff --git a/executor/.gitignore b/executor/.gitignore index c03cd0a0a..d3448fb7a 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -2,7 +2,12 @@ /program_artifacts/rust # Real-block fixtures (~1 MB): generated by `make ethrex-real-block-fixture` # from an ethrex-replay cache, never committed. See tooling/ethrex-real-block. +# One pattern per network the converter accepts (mainnet/hoodi/sepolia β€” it +# rejects anything else), so repointing ETHREX_REAL_BLOCK_NETWORK in the Makefile +# cannot silently make a ~1 MB fixture committable. +/tests/ethrex_mainnet_*.bin /tests/ethrex_hoodi*.bin +/tests/ethrex_sepolia_*.bin /tests/ethrex_bench_*.bin # _4 is committed (~17 KB): used by `make recursion-profile-block-input` and # scripts/bench_recursion_scaling.sh. Other sizes stay ignored β€” scaling.sh diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 0e820f5bf..e8f1467ad 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -11,6 +11,25 @@ # own proof (required when REF_A changes the proof format); 0 = force one # shared proof (best precision); auto = share if the PR binary can verify the # baseline's proof, else fall back to per-side. +# WORKLOAD=synthetic|real (default synthetic): which block the proof under +# test is built from. See "Workload" below. +# EPOCH_SIZE_LOG2= (default 21): continuation epoch size, WORKLOAD=real only. +# +# Workload. What this script measures is VERIFY cost, and verify cost is structural +# in the proof β€” table mix, trace lengths, query count β€” so it is the block that +# decides what is being measured, not the loop around it. The synthetic 20-transfer +# default is 20 plain transfers: ecrecover-heavy, near-empty state (measured: 14.2M +# cycles, 411 keccak calls, 80 ecsm calls). A real block inverts that mix (168.3M +# cycles, 9,046 keccak, 44 ecsm), so a verifier change can move the two differently. +# +# WORKLOAD=real is therefore the honest number and WORKLOAD=synthetic the cheap one; +# the default is cheap because the real block must be proven with continuations +# (~15 min per side, once, then cached in $WORK) before any verify run happens. +# Both sides always use the same block, so a comparison is never mixed. +# +# `/bench-verify` runs the synthetic default: that job already budgets 90 minutes +# for the verifier bench plus a 70-minute recursion step, and two real-block proves +# do not fit under the cap. Run WORKLOAD=real by hand on the bench server. set -euo pipefail @@ -23,9 +42,16 @@ REF_A="$1" REF_B="${2:-origin/main}" N_PAIRS="${3:-20}" BENCH_FEATURES="${BENCH_FEATURES:-jemalloc-stats}" +WORKLOAD="${WORKLOAD:-synthetic}" +EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-21}" +case "$WORKLOAD" in + synthetic|real) ;; + *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;; +esac ELF_REL="executor/program_artifacts/rust/ethrex.elf" -INPUT_REL="executor/tests/ethrex_bench_20.bin" +# Resolved after the cd to the repo root (WORKLOAD=real reads it from the Makefile). +INPUT_REL="" WORK="/tmp/verify_run" WT="/tmp/verify_wt" PROOF_B="$WORK/proof_b.bin" # baseline's proof (cached in $WORK, keyed like the binaries) @@ -48,6 +74,18 @@ if [ $((N_PAIRS % 2)) -ne 0 ]; then fi echo " pairs=$N_PAIRS (=$((N_PAIRS * 2)) verify runs)" +# The real block's name lives in the Makefile and nowhere else, so repointing the +# benchmark to a different block never needs an edit here. +if [ "$WORKLOAD" = "real" ]; then + INPUT_REL="$(make -s print-real-block-fixture)" + CONT_ARGS="--continuations --epoch-size-log2 $EPOCH_SIZE_LOG2" + echo " workload=real $INPUT_REL (continuations, epoch 2^$EPOCH_SIZE_LOG2)" +else + INPUT_REL="executor/tests/ethrex_bench_20.bin" + CONT_ARGS="" + echo " workload=synthetic $INPUT_REL (set WORKLOAD=real for a real block)" +fi + mkdir -p "$WORK" # --- 1. Guest ELF + fixture (identical for both sides; build once if missing) --- @@ -57,9 +95,16 @@ if [ ! -f "$ELF_REL" ]; then make "$ELF_REL" fi if [ ! -f "$INPUT_REL" ]; then - echo "==> Generating ethrex 20-transfer fixture (missing)" - ( cd tooling/ethrex-fixtures && cargo build --release ) - tooling/ethrex-fixtures/target/release/ethrex-fixtures 20 "$INPUT_REL" distinct + if [ "$WORKLOAD" = "real" ]; then + # ~1 MB and gitignored, so it is never in a fresh checkout. Cold runs also + # download a ~1.5 MB ethrex-replay cache (pinned rev, so the input can't drift). + echo "==> Generating ethrex real-block fixture (missing; downloads a cache on a cold run)" + make ethrex-real-block-fixture + else + echo "==> Generating ethrex 20-transfer fixture (missing)" + ( cd tooling/ethrex-fixtures && cargo build --release ) + tooling/ethrex-fixtures/target/release/ethrex-fixtures 20 "$INPUT_REL" distinct + fi fi ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")" INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" @@ -107,8 +152,16 @@ fi # per-side proofs (each binary proves and verifies its own). PROVE_PER_SIDE overrides. PROVE_PER_SIDE="${PROVE_PER_SIDE:-auto}" +# $CONT_ARGS is empty for the synthetic workload, so both paths below are the +# original command there. WORKLOAD=real adds --continuations on BOTH prove and +# verify: a continuation bundle is a different proof type, and `verify` without +# the flag would try to deserialize it as a monolithic proof and fail. +VERIFY_ARGS="" +if [ "$WORKLOAD" = "real" ]; then VERIFY_ARGS="--continuations"; fi + prove_once() { # $1=binary $2=proof-path - if ! "$1" prove "$ELF" --private-input "$INPUT" -o "$2" --time >"$WORK/prove_$(basename "$2").log" 2>&1; then + # shellcheck disable=SC2086 # CONT_ARGS is a deliberate multi-word flag list + if ! "$1" prove "$ELF" --private-input "$INPUT" $CONT_ARGS -o "$2" --time >"$WORK/prove_$(basename "$2").log" 2>&1; then echo "ERROR: prove failed for $1. Tail of log:" >&2 tail -20 "$WORK/prove_$(basename "$2").log" >&2 exit 1 @@ -116,7 +169,8 @@ prove_once() { # $1=binary $2=proof-path } verify_time() { # $1=binary $2=proof-path -> echoes time on success, empty on failure (never exits) local out - out="$("$1" verify "$2" "$ELF" --time 2>&1)" || true + # shellcheck disable=SC2086 + out="$("$1" verify "$2" "$ELF" $VERIFY_ARGS --time 2>&1)" || true printf '%s\n' "$out" | grep -o 'Verification time: [0-9.]*' | awk '{print $3}' || true } run_verify() { # $1=binary $2=proof-path -> echoes verification time (s), exits on failure @@ -124,7 +178,8 @@ run_verify() { # $1=binary $2=proof-path -> echoes verification time (s), exits t="$(verify_time "$1" "$2")" if [ -z "$t" ]; then echo "ERROR: could not parse 'Verification time' from '$1 verify $2':" >&2 - "$1" verify "$2" "$ELF" --time >&2 2>&1 || true + # shellcheck disable=SC2086 + "$1" verify "$2" "$ELF" $VERIFY_ARGS --time >&2 2>&1 || true echo "HINT: if REF_A changes the proof format, run with PROVE_PER_SIDE=1." >&2 exit 1 fi @@ -133,13 +188,15 @@ run_verify() { # $1=binary $2=proof-path -> echoes verification time (s), exits # Both sides prove their own proof (needed for the proof-size row; per-side verify # needs both). Proofs are cached in $WORK like the binaries, marker -# " ". Bytes are non-deterministic (parallel grinding) -# but size + verify cost are structural, so reusing a cached proof is valid. The prove -# call passes no proof-option flags; if it ever gains one (--blowup, ...), add it to the marker. +# " ". Bytes are non-deterministic +# (parallel grinding) but size + verify cost are structural, so reusing a cached proof +# is valid. $CONT_ARGS is in the marker because it changes the proof TYPE, and without +# it a WORKLOAD switch would silently reuse the other workload's proof; keep adding any +# future proof-option flag (--blowup, ...) the same way. sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum; else shasum -a 256; fi; } PROOF_KEY_INPUT="$(cat "$ELF" "$INPUT" | sha256_of | cut -c1-16)" prove_cached() { # $1=binary $2=proof-path $3=sha - local marker="$3 $BENCH_FEATURES $PROOF_KEY_INPUT" + local marker="$3 $BENCH_FEATURES $PROOF_KEY_INPUT $CONT_ARGS" if [ "${REBUILD:-0}" != "1" ] && [ -f "$2" ] && [ "$(cat "$2.sha" 2>/dev/null)" = "$marker" ]; then echo "==> Reusing cached proof for ${3:0:10} ($(basename "$2"))" else @@ -166,7 +223,8 @@ per_side_note="" case "$PROVE_PER_SIDE" in 1) per_side=1; per_side_note="forced via PROVE_PER_SIDE=1" ;; 0) per_side=0 ;; - *) probe="$("$WORK/cli_A" verify "$PROOF_B" "$ELF" --time 2>&1 || true)" + *) # shellcheck disable=SC2086 + probe="$("$WORK/cli_A" verify "$PROOF_B" "$ELF" $VERIFY_ARGS --time 2>&1 || true)" if printf '%s\n' "$probe" | grep -q 'Verification time'; then per_side=0 # PR verifies main's proof -> shared elif printf '%s\n' "$probe" | grep -q 'Failed to deserialize'; then @@ -208,7 +266,13 @@ done # Proofs are kept in $WORK as a cache (invalidated by their .sha markers), not deleted. # --- 4. Paired t-test + robust median/Wilcoxon (same stats as bench_abba.sh) --- -SIZE_A="$SIZE_A" SIZE_B="$SIZE_B" MODE="$MODE" PER_SIDE_NOTE="$per_side_note" python3 - "$WORK/pairs.csv" <<'PY' +if [ "$WORKLOAD" = "real" ]; then + WORKLOAD_LABEL="real block \`$(basename "$INPUT_REL")\` Β· continuations Β· epoch 2^$EPOCH_SIZE_LOG2" +else + WORKLOAD_LABEL="synthetic ethrex 20 transfers (set WORKLOAD=real for a real block)" +fi +SIZE_A="$SIZE_A" SIZE_B="$SIZE_B" MODE="$MODE" PER_SIDE_NOTE="$per_side_note" \ +WORKLOAD_LABEL="$WORKLOAD_LABEL" python3 - "$WORK/pairs.csv" <<'PY' import sys, csv, math, os rows = list(csv.DictReader(open(sys.argv[1]))) @@ -296,6 +360,9 @@ mode = os.environ.get('MODE', 'shared') per_side_note = os.environ.get('PER_SIDE_NOTE', '') print("\n=== Verify ABBA result ===") +# A verify time is only interpretable against the block that produced the proof, +# and these results get pasted into PRs, so the workload travels with the number. +print(f"Workload: {os.environ.get('WORKLOAD_LABEL', 'unknown')}") print() # Proof size row: exact (the .bin byte size), no ABBA. - = PR smaller = better. diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index ddadf53fd..41eb80d33 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -13,6 +13,17 @@ # # USAGE (on the bench server): # scripts/perf_diff.sh REF_A [REF_B=origin/main] +# Env: WORKLOAD=synthetic|real (default synthetic) picks the block to profile; +# EPOCH_SIZE_LOG2= (default 21) sizes the epoch, WORKLOAD=real only. +# +# Pick the workload that matches the run you are localizing, because the symbol +# mix follows the block: the synthetic default is 20 plain transfers (measured: +# 14.2M cycles, 411 keccak calls, 80 ecsm calls) and a real block inverts that +# (168.3M cycles, 9,046 keccak, 44 ecsm), so a hot symbol in one need not be hot +# in the other. WORKLOAD=real also switches to a continuation prove (monolithic +# would need ~500 GB at that trace length), which is ~15 min per recording β€” five +# recordings, so budget over an hour. +# # Produces: # - two perf-diff tables (recorded twice per side, interleaved B A B A β€” # symbols whose delta repeats across both tables are real, one-off @@ -29,9 +40,14 @@ if [ $# -lt 1 ]; then fi REF_A="$1" REF_B="${2:-origin/main}" +WORKLOAD="${WORKLOAD:-synthetic}" +EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-21}" +case "$WORKLOAD" in + synthetic|real) ;; + *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;; +esac ELF_REL="executor/program_artifacts/rust/ethrex.elf" -INPUT_REL="executor/tests/ethrex_bench_20.bin" WORK="/tmp/perf_diff" WT="/tmp/perf_diff_wt" PROOF="/tmp/perf_diff_proof.bin" @@ -39,9 +55,30 @@ PROOF="/tmp/perf_diff_proof.bin" ROOT="$(git rev-parse --show-toplevel)" cd "$ROOT" +# The real block's name lives in the Makefile and nowhere else, so repointing the +# benchmark to a different block never needs an edit here. +if [ "$WORKLOAD" = "real" ]; then + INPUT_REL="$(make -s print-real-block-fixture)" + CONT_ARGS="--continuations --epoch-size-log2 $EPOCH_SIZE_LOG2" +else + INPUT_REL="executor/tests/ethrex_bench_20.bin" + CONT_ARGS="" +fi + command -v perf >/dev/null 2>&1 || { echo "ERROR: perf not installed (linux-tools)." >&2; exit 1; } [ -f "$ELF_REL" ] || { echo "ERROR: missing $ELF_REL β€” run bench_abba.sh once (it builds the guest)." >&2; exit 1; } -[ -f "$INPUT_REL" ] || { echo "ERROR: missing $INPUT_REL β€” run bench_abba.sh once (it builds the fixture)." >&2; exit 1; } +if [ ! -f "$INPUT_REL" ]; then + if [ "$WORKLOAD" = "real" ]; then + # ~1 MB and gitignored, so it is never in a fresh checkout; generate rather than + # abort. A cold run also downloads a ~1.5 MB ethrex-replay cache (pinned rev). + echo "==> Generating ethrex real-block fixture (missing; downloads a cache on a cold run)" + make ethrex-real-block-fixture + else + echo "ERROR: missing $INPUT_REL β€” run bench_abba.sh once (it builds the fixture)." >&2 + exit 1 + fi +fi +echo "==> Workload: $WORKLOAD ($INPUT_REL${CONT_ARGS:+, continuations epoch 2^$EPOCH_SIZE_LOG2})" echo "==> Refs" git fetch origin --quiet || echo "WARNING: 'git fetch origin' failed -- resolving against possibly-stale local refs." >&2 @@ -86,14 +123,16 @@ fi # --- Record: warmup, then B A B A (interleaved so drift hits both sides) --- record() { # $1=binary $2=out.data + # shellcheck disable=SC2086 # CONT_ARGS is a deliberate multi-word flag list (empty when synthetic) perf record -F 599 -o "$WORK/$2" -- \ - "$WORK/$1" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time \ + "$WORK/$1" prove "$ELF_REL" --private-input "$INPUT_REL" $CONT_ARGS -o "$PROOF" --time \ >"$WORK/$2.log" 2>&1 rm -f "$PROOF" grep -o 'Proving time: [0-9.]*' "$WORK/$2.log" || true } echo "==> Warmup (B, not recorded)" -"$WORK/cli_B" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time >/dev/null 2>&1 +# shellcheck disable=SC2086 +"$WORK/cli_B" prove "$ELF_REL" --private-input "$INPUT_REL" $CONT_ARGS -o "$PROOF" --time >/dev/null 2>&1 rm -f "$PROOF" echo "==> Recording B (main), run 1"; record cli_B B1.data echo "==> Recording A (PR), run 1"; record cli_A A1.data diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index aaed79ae1..dd6e589e3 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -75,6 +75,30 @@ That checksum is enforced, not just documented: `conversion_is_reproducible` asserts it. The pinned commit keeps the *input* immutable; the digest keeps the *derivation* honest. +## What benchmarks with it + +| Where | How to run it | Cost | +|---|---|---| +| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~15 min | +| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~15 min per side, then cached | +| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so >1 h | + +None of them hardcode the fixture path β€” they read it from +`make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when +the (gitignored) `.bin` is absent. + +**Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic +prove grows ~3.1 GB per million cycles on this workload family (measured on the +bench server: 2007 MB per transfer at RΒ² = 0.998 across 4β†’20 transfers), and the +real block is 168.3M cycles β€” roughly 500 GB. `--continuations` makes peak heap a +function of the epoch size instead of the trace length, so the same block fits in +~16 GB at epoch 2^21. + +Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in +~25s against ~15 min, and the bench runner is single and serialized across all PRs. +The synthetic number is a fast screen; the real block is the number that means +something. + ## Getting a cache for a different block The cache format is `ethrex-replay`'s, so use that tool to produce one β€” it @@ -98,9 +122,36 @@ witness is only ever validated against whichever config we chose. The converter refuses instead; `unmappable_network_is_rejected` pins that. Then point this tool at the resulting JSON. Adopting a different block as the -default is an edit, not a flag: `ETHREX_REAL_BLOCK` and `ETHREX_REPLAY_REV` in the -Makefile, the `hoodi` in the cache URL, `CACHE` and the assertions in -`src/main.rs`, and `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests`. +default is an edit, not a flag. Three of the four edits are the block's identity; +the fourth is the digest that proves the conversion still matches. + +**1. The Makefile β€” this is what moves every benchmark.** Two lines: + +```make +ETHREX_REAL_BLOCK_NETWORK := hoodi # -> mainnet +ETHREX_REAL_BLOCK := 1265656 # -> 25453112 +``` + +Everything downstream is derived from them: the cache path, the download URL, the +fixture name, and β€” through `make -s print-real-block-fixture` β€” the benchmark +scripts and `benchmark-pr.yml`. Nothing else names the block. Re-pin +`ETHREX_REPLAY_REV` in the same edit if the new cache landed in a later commit. + +**2. `src/main.rs`** β€” `CACHE` and the block assertions in +`conversion_is_reproducible`, including the **sha256**. The digest cannot be +guessed: run `make ethrex-real-block-fixture`, take the printed `sha256:` line, +and paste it in. That is the check that keeps the derivation honest. + +**3. `tooling/ethrex-tests`** β€” `REAL_BLOCK_FIXTURE`. + +**4. Nothing in `executor/.gitignore`** β€” it already ignores every accepted +network's fixture name, so a repointed ~1 MB fixture cannot become committable by +accident. + +Benchmark comparability does not survive the swap, and that is intentional rather +than a wrinkle to work around: `benchmark-pr.yml` records which block it measured +and refuses to diff a PR against a baseline that measured a different one, so the +first run after a repoint reports one-sided numbers until main republishes. Then run **`make test-ethrex`**, not just this crate's tests. A new block is only usable if it needs no accelerator the guest lacks, and this crate cannot tell you From 23321743fad6cd42655fd45a494785df99fb766f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:00:25 -0300 Subject: [PATCH 11/27] build(ethrex): fetch the real-block fixture, and take it out of the PR gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes with one root: the real-block fixture is a benchmark INPUT, and it was being treated as a build artifact and a correctness gate. Fetch it instead of building it. `make ethrex-real-block-fixture` now downloads the finished .bin from a pinned URL and verifies its sha256 before moving it into place, mirroring prepare-sysroot (same dual sha256sum/shasum handling, same hard error when neither exists). Off the fixture's path, and therefore deleted from it: the 3.8 MB ethrex-replay cache download, the converter build, the ~335-package ethrex host dependency tree, and the ETHREX_REPLAY_REV pin. That also unblocks the block we actually want. Upstream hosts an ethrex-replay cache for Hoodi and nothing else, so a mainnet block cannot be produced by the convert-locally route at all β€” generating its cache takes ~4 min and ~700 archive RPC calls. Fetching a verified binary makes the benchmark block independent of what lambdaclass happens to host, and independent of this crate's own pins, which stay on Hoodi because that is what conversion_is_reproducible reads. ETHREX_REAL_BLOCK_FIXTURE_URL is deliberately left EMPTY β€” the artifact is not hosted yet, and a guessed URL fails with a 404 nobody can act on. Unset, it fails with an actionable message, and every consumer degrades instead of breaking: benchmark-pr.yml warns and skips its real-block section rather than reddening every push to main, and the usability screen skips too. Adopting mainnet 25453112 once hosted is four Makefile lines plus REAL_BLOCK_FIXTURE in ethrex-tests; its digest and measured cost are recorded in the crate README. Take it out of the required gate. pr_main.yaml's test-executor job no longer generates the fixture, no longer builds tooling/ethrex-real-block, and skips both real-block tests. No product code reads this fixture: it has to be right when it changes, not on every PR, and it was costing every PR a network download plus a cold ~335-package build (blst, c-kzg, secp256k1-sys, malachite, ark-ff/asm) inside the branch-protection gate. Those checks are not dropped, they move to a new path-filtered workflow (.github/workflows/ethrex-real-block.yml) triggered by tooling/ethrex-real-block, tooling/ethrex-tests and the Makefile β€” i.e. by the things that can actually invalidate them. It runs the converter's tests (host-side parity, network rejection, reproducibility digest) and the block-usability screen (test_ethrex_real_block_native, which is what catches a block needing precompile 0x0a). no_kzg_backend_linked stays in the PR gate: it is a pure unit test and it is the property the usability screen depends on, so it should fail on the PR that breaks it. This supersedes part of 50dfca46: `tooling/ethrex-real-block -> target` is gone from test-executor's rust-cache workspaces, since that job no longer builds the workspace; the new job carries its own cache entry under its own key. `tooling/ethrex-tests -> target` stays and is still doing its job. Corrections while in here, all against a CURRENT guest ELF (counts move ~14% with ELF vintage, so every quoted figure now names its ELF): - synthetic 9.06M cycles, real 147.5M β€” a 16x ratio, not the 11.9x that came from comparing measurements taken on two different ELF vintages - monolithic peak heap ~4.9 GB/Mcycle from the measured growth fit, so ~700 GB for the real block rather than ~500 GB - continuation peak ~21 GB at epoch 2^21 (15.8 GB working set plus ~69 MB/epoch of accumulated proofs), not ~16 GB; the bundle is ~5 GB on disk and needs rkyv pointer_width_64 to serialize past 2 GiB - ~13 min per prove, not ~15 - bench_vs/run_ethrex.sh claimed ethrex_10_transfers is ~42M cycles and OOMs at 36 GB. It is 6.8M; the figure predates ecrecover becoming an ECSM accelerator. --- .github/workflows/benchmark-pr.yml | 79 ++++++----- .github/workflows/ethrex-real-block.yml | 113 ++++++++++++++++ .github/workflows/pr_main.yaml | 49 ++++--- Makefile | 156 +++++++++++++++++----- bench_vs/run_ethrex.sh | 10 +- scripts/bench_verify.sh | 21 +-- scripts/perf_diff.sh | 20 +-- tooling/ethrex-real-block/README.md | 166 ++++++++++++++++-------- 8 files changed, 451 insertions(+), 163 deletions(-) create mode 100644 .github/workflows/ethrex-real-block.yml diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 5229ce517..81702d78a 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -45,28 +45,36 @@ env: # "Generate ethrex bench fixtures" step). # # This is not a representative block β€” 20 plain transfers are ecrecover-heavy and - # touch almost no state (measured, same ELF: 14.2M cycles, 411 keccak calls, 80 - # ecsm calls). It stays the per-comment default only because it proves in ~25s; - # the representative number is REAL_BLOCK below, which runs on push/dispatch and - # on an explicit `/bench-real`. + # touch almost no state: 9.06M cycles, 411 keccak calls, 80 ecsm calls. It stays + # the per-comment default only because it proves in ~25s; the representative + # number is the real block below, which runs on push/dispatch and `/bench-real`. + # + # Cycle counts here are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"). + # They move ~14% with guest-ELF vintage, so a count quoted against a different ELF + # will read as a regression β€” always pin the ELF when repeating one. ELF: executor/program_artifacts/rust/ethrex.elf INPUT: executor/tests/ethrex_bench_20.bin - # Headline (representative) workload: a real Ethereum block, converted from an - # ethrex-replay cache by tooling/ethrex-real-block. Same measured ELF: 168.3M - # cycles, 9,046 keccak calls, 44 ecsm calls β€” i.e. ~12x the work of the synthetic - # block with a ~40x different keccak:ecrecover mix. That is the whole point: a - # prover change can move the synthetic number and the real one in opposite - # directions. + # Headline (representative) workload: a real Ethereum block. 147.5M cycles, 9,046 + # keccak calls, 44 ecsm calls β€” ~16x the work of the synthetic block with a ~40x + # different keccak:ecrecover mix. That is the whole point: a prover change can move + # the synthetic number and the real one in opposite directions. # # The path is NOT hardcoded here: it is resolved from the Makefile # (`make -s print-real-block-fixture`) into REAL_INPUT at run time, so repointing - # the block is a Makefile edit and nothing else. The .bin is ~1 MB and gitignored, - # so the job generates it (see "Generate ethrex real-block fixture"). + # the block is a Makefile edit and nothing else. The ~1 MB .bin is gitignored and + # FETCHED by URL + sha256 (see "Fetch ethrex real-block fixture"); while that URL + # is unset the whole section degrades to a warning rather than failing the job. # # Continuations are mandatory here, not a preference: a monolithic prove costs - # ~3.1 GB of peak heap per million cycles on this workload family (measured - # growth slope, R^2 = 0.998), so 168M cycles would need ~500 GB. `--continuations` - # makes peak heap flat in the epoch size instead of the trace length. + # ~4.9 GB of peak heap per million cycles on this workload family (from the + # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so 147.5M + # cycles would need ~700 GB. `--continuations` makes peak heap a function of the + # epoch size instead of the trace length: ~21 GB here (a 15.8 GB epoch-2^21 + # working set plus ~69 MB per epoch of accumulated proofs over 68 epochs). + # + # Budget ~5 GB of disk for the bundle each run β€” hence the `rm -f` after every + # prove. Serializing it past 2 GiB needs rkyv `pointer_width_64`, so a PR branch + # predating that fix fails here at write time rather than mismeasuring. REAL_BLOCK_EPOCH_LOG2: "21" BENCH_RUNS_REAL: 1 BENCH_RUNS_PR: 3 @@ -171,20 +179,30 @@ jobs: # Real-block benchmark: same gating as growth (/bench-real, push to main, # workflow_dispatch), and for the same reason β€” it is far too slow for the - # per-comment tier. One continuation prove of the real block is ~15 min of - # CPU on this runner (extrapolated from the ~7 min cont100 figure in - # bench-abba.yml at ~2.5x the cycles), against ~25s for the synthetic - # block, and the runner is single and serialized across all PRs. + # per-comment tier. One continuation prove of the real block is ~13 min of + # CPU on this runner (147.5M cycles at the 5.31-5.62 s/Mcycle measured on + # this box in run 30631871174), against ~25s for the synthetic block, and + # the runner is single and serialized across every /bench, /bench-abba and + # /bench-verify in the repo. # # Because push-to-main always runs it, the baseline artifact carries a # real-block number, so `/bench-real` on a PR pays for the PR side only. + RUN_REAL=false if [ "$EVENT_NAME" = "issue_comment" ] && echo "$COMMENT_BODY" | grep -q '^/bench-real'; then - echo "run_real=true" >> "$GITHUB_OUTPUT" + RUN_REAL=true elif [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then - echo "run_real=true" >> "$GITHUB_OUTPUT" - else - echo "run_real=false" >> "$GITHUB_OUTPUT" + RUN_REAL=true + fi + + # The fixture is FETCHED from a pinned URL + sha256, not built (see the + # Makefile). While that URL is unset the block cannot be obtained at all, so + # degrade to the synthetic sections with a warning rather than failing β€” + # otherwise every push to main goes red over an upload nobody in CI can do. + if [ "$RUN_REAL" = "true" ] && [ -z "$(make -s print-real-block-fixture-url)" ]; then + echo "::warning::Real-block benchmark skipped: ETHREX_REAL_BLOCK_FIXTURE_URL is unset in the Makefile." + RUN_REAL=false fi + echo "run_real=$RUN_REAL" >> "$GITHUB_OUTPUT" # Resolve the real-block fixture path from its single source of truth (the # Makefile) and pin it in the job env. Captured HERE, before the baseline @@ -226,13 +244,14 @@ jobs: echo "Using $RUNS iterations, TABLE_PARALLELISM=default" fi - - name: Generate ethrex real-block fixture + - name: Fetch ethrex real-block fixture if: steps.config.outputs.run_real == 'true' run: | - # ~1 MB and gitignored, so it is never in the checkout: build it rather - # than failing on a missing file, the same way the synthetic fixtures are - # generated above. Cold runs also fetch a ~1.5 MB ethrex-replay cache over - # the network (pinned to an immutable rev, so the input can't drift). + # ~1 MB, gitignored, and never in the checkout: fetch it rather than failing + # on a missing file, the same way the synthetic fixtures are generated above. + # This is a URL + sha256 download, not a build β€” no converter, no ethrex host + # dependency tree, no ethrex-replay cache. The step is already gated on the + # URL being set (see "Determine run count"). # # Untracked, so like the ELF it survives `git checkout origin/main` and both # sides prove the identical block. @@ -970,7 +989,7 @@ jobs: const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Synthetic screen β€” 20 plain transfers, ~12x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; + body += `Synthetic screen β€” 20 plain transfers, ~16x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Ξ” |\n`; body += `|--------|------|----|---|\n`; body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; @@ -1096,7 +1115,7 @@ jobs: // --- Footer --- if (!realTime) { body += `\n> 🧱 **This ran on synthetic blocks only.** Comment \`/bench-real\` to prove a real Ethereum block `; - body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~15 min to the run.\n`; + body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~13 min to the run.\n`; } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; diff --git a/.github/workflows/ethrex-real-block.yml b/.github/workflows/ethrex-real-block.yml new file mode 100644 index 000000000..8331d8327 --- /dev/null +++ b/.github/workflows/ethrex-real-block.yml @@ -0,0 +1,113 @@ +name: ethrex real block + +# Validation for the real-block benchmark fixture and the converter that produces +# it. Deliberately NOT part of the required PR gate (pr_main.yaml): the fixture is a +# benchmark input, not a correctness input β€” no product code reads it β€” so it needs +# to be right when it changes, not on every PR. Running it there cost every PR a +# network download plus a cold build of ~335 packages (blst, c-kzg and secp256k1-sys +# C builds, malachite, ark-ff/asm). +# +# It fires on the things that can actually invalidate it: the converter, the ethrex +# host-reference tests, and the Makefile (which holds the block pin, the fixture URL +# and its sha256). +on: + workflow_dispatch: + pull_request: + branches: ["**"] + paths: + - 'tooling/ethrex-real-block/**' + - 'tooling/ethrex-tests/**' + - 'Makefile' + - '.github/workflows/ethrex-real-block.yml' + push: + branches: ["main"] + paths: + - 'tooling/ethrex-real-block/**' + - 'tooling/ethrex-tests/**' + - 'Makefile' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + converter: + name: Real-block converter tests + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Rust Environment + uses: ./.github/actions/setup-rust + + - name: Cache cargo build artifacts + uses: Swatinem/rust-cache@v2 + with: + # Own key, not pr_main.yaml's "lambda-vm-test": a shared key across jobs + # that build different crate sets causes recompilation on both sides. + shared-key: "lambda-vm-ethrex-real-block" + cache-all-crates: "true" + # Detached workspace (own Cargo.lock, own target dir), so the default + # `. -> target` would miss it and rebuild the ethrex tree every run. + workspaces: | + tooling/ethrex-real-block -> target + + # Downloads the pinned ethrex-replay cache (the converter's test input) and + # runs the crate's tests: host-side parity through the guest's own + # `LambdaVmEcsmCrypto`, the unmappable-network rejection, and the + # reproducibility digest. Note these do NOT screen KZG β€” this crate's graph + # links c-kzg via ethrex-config, so point evaluation (0x0a) resolves here and + # to nothing in the guest. The block-usability job below is what covers that. + - name: Run converter tests + run: make test-ethrex-real-block-converter + + block-usable: + name: Real-block usability screen + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Rust Environment + uses: ./.github/actions/setup-rust + + - name: Cache cargo build artifacts + uses: Swatinem/rust-cache@v2 + with: + shared-key: "lambda-vm-ethrex-real-block-usable" + cache-all-crates: "true" + workspaces: | + tooling/ethrex-tests -> target + + # Fetch-and-verify, not build: no converter, no ethrex-replay cache, no rev pin. + # Skipped while the fixture has no host β€” the screen below is the only consumer, + # and failing the job on an unset URL would block PRs on an upload nobody in the + # PR can perform. Remove this guard once ETHREX_REAL_BLOCK_FIXTURE_URL is set. + - name: Fetch real-block fixture + id: fixture + run: | + if [ -z "$(make -s print-real-block-fixture-url)" ]; then + echo "::warning::ETHREX_REAL_BLOCK_FIXTURE_URL is unset β€” skipping the real-block usability screen. Set it in the Makefile once the .bin is hosted." + echo "present=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + make ethrex-real-block-fixture + echo "present=true" >> "$GITHUB_OUTPUT" + + # `test_ethrex_real_block_native` is the screen that makes a block USABLE rather + # than merely realistic: this crate links no KZG backend (pinned by + # `no_kzg_backend_linked`, which runs in pr_main.yaml), so a block calling point + # evaluation (0x0a) diverges from consensus and fails here instead of silently + # passing and then failing in the guest. + # + # `test_ethrex_real_block_vm` stays excluded: it drives the block through the + # guest ELF, needs the RV64 toolchain, and its runtime is unmeasured. + - name: Screen the block against the guest's precompile surface + if: steps.fixture.outputs.present == 'true' + run: | + cd tooling/ethrex-tests && \ + cargo test --release test_ethrex_real_block_native -- --include-ignored diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index 0e71c2165..7510a9497 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -56,15 +56,17 @@ jobs: with: shared-key: "lambda-vm-test" cache-all-crates: "true" - # Both ethrex tool workspaces are detached (own Cargo.lock, own target - # dir), so the default `. -> target` misses them and their release - # trees rebuild from scratch on every run β€” ~335 packages for - # ethrex-real-block alone, including the blst, c-kzg and secp256k1-sys - # C builds, malachite and ark-ff/asm. `cache-all-crates` only covers - # the registry, not compiled artifacts. + # ethrex-tests is a detached workspace (own Cargo.lock, own target dir), so + # the default `. -> target` misses it and its release tree would rebuild + # from scratch every run. `cache-all-crates` only covers the registry, not + # compiled artifacts. + # + # ethrex-real-block is deliberately NOT here: this job no longer builds it. + # That workspace is the expensive one (~335 packages, including the blst, + # c-kzg and secp256k1-sys C builds, malachite and ark-ff/asm) and it now + # builds only in ethrex-real-block.yml, which carries its own cache entry. workspaces: | . -> target - tooling/ethrex-real-block -> target tooling/ethrex-tests -> target - name: Cache compiled ASM ELF artifacts @@ -111,33 +113,30 @@ jobs: run: | cargo test --release -p executor test_ckzg -- --ignored - # The real-block fixture (~1 MB) is gitignored and generated on demand; - # test_ethrex_real_block_native below needs it present. This also - # downloads the ethrex-replay cache the converter's own tests read. - - name: Generate ethrex real-block fixture - run: make ethrex-real-block-fixture - - # Detached workspace. Executes the block through `LambdaVmEcsmCrypto`, the - # guest's own `Crypto` impl, and pins the fixture's sha256. Note it does NOT - # screen KZG (its graph links c-kzg via ethrex-config β€” see that crate's - # Cargo.toml); the ethrex-tests step below is what covers 0x0a. - - name: Run ethrex real-block converter tests (detached workspace) - run: | - cd tooling/ethrex-real-block && cargo test --release - # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature, which must not # feature-unify with the main workspace's aligned proof format), so run # them from that directory to use its isolated Cargo.lock. The guest ELF # and committed fixtures are already present from the steps above. # --include-ignored also runs the heavier synthetic-block test. - # `test_ethrex_real_block_vm` is skipped: it drives a real block through - # the VM and its runtime is not yet measured, so it stays opt-in rather - # than sitting in the PR gate. Run it manually on a build server. + # + # `--skip test_ethrex_real_block` is a substring match, so it drops BOTH + # real-block tests (the `_vm` and `_native` ones) and with them this job's need + # for the real-block fixture. That fixture is a benchmark input: it has to be + # right once, not on every PR, and fetching plus screening it here put a network + # download and a ~335-package build in the required gate for an artifact with no + # bearing on correctness. Both moved to ethrex-real-block.yml, which runs when + # the converter or the block pin actually changes. `no_kzg_backend_linked` still + # runs here β€” it is a pure unit test, and it is the property the real-block + # screen over there depends on. + # + # This is `make test-ethrex-offline` spelled out, matching the other test steps + # in this job: they all bypass make so its dependency check cannot decide the + # cache-restored ELFs are stale and trigger a rebuild. - name: Run ethrex host-reference tests (detached workspace) run: | cd tooling/ethrex-tests && \ - cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm + cargo test --release -- --include-ignored --skip test_ethrex_real_block test-cli: name: CLI tests diff --git a/Makefile b/Makefile index be6e2c8bc..c04b36edb 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-mat test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \ -print-real-block-fixture +print-real-block-fixture print-real-block-fixture-url \ +test-ethrex-real-block-converter regen-real-block-fixture UNAME := $(shell uname) @@ -272,39 +273,43 @@ test-asm: compile-programs-asm test-rust: compile-programs-rust cargo test -p executor --test rust -# Real-block fixture: a genuine Hoodi block (4.4M gas, 11 txs, ~124 KB of -# contract bytecode, 1705 state-trie nodes), as opposed to the synthetic -# N-plain-transfer blocks from tooling/ethrex-fixtures. ~1 MB, so it is -# generated on demand and gitignored rather than committed. +# ===== Real-block benchmark fixture ===== # -# The source is ethrex-replay's cache JSON, not that tool's own rkyv output: -# ethrex-replay tracks ethrex `main`, where `ProgramInput` has diverged from the -# rev our guest pins, so only the JSON is safe to read across the two. +# A genuine Ethereum block (Hoodi 1265656: 4.4M gas, 11 txs, ~124 KB of contract +# bytecode, 1705 state-trie nodes), as opposed to the synthetic N-plain-transfer +# blocks from tooling/ethrex-fixtures. ~1 MB, gitignored. # -# Which block the benchmarks run is these TWO lines and nothing else: every path -# below (cache file, download URL, fixture name) is derived from them, and the -# benchmark scripts / CI resolve the fixture through `make -s print-real-block-fixture` -# rather than spelling it out. Repointing to, say, mainnet 25453112 is -# `ETHREX_REAL_BLOCK_NETWORK := mainnet` + the new number + a matching -# `ETHREX_REPLAY_REV`, then the test-side pins listed in -# tooling/ethrex-real-block/README.md ("Getting a cache for a different block"). +# FETCHED, not built. Downloading the finished .bin and checking its sha256 is the +# same contract as prepare-sysroot above, and it takes the converter, the ~335-package +# ethrex host dependency tree, the ethrex-replay cache and the `rev` pin off the path +# of everyone who just wants to run a benchmark. It also decouples the block from +# what upstream happens to host: ethrex-replay publishes a cache for Hoodi and +# nothing else, so a mainnet block is unreachable by the convert-locally route but +# trivial by this one. +# +# The converter still exists and is still tested β€” see "Real-block converter" below. +# It is now a regeneration tool (ethrex rev bumps, roughly twice a year), not a +# build step. +# +# Which block the benchmarks run is these THREE lines and nothing else. Every path +# below is derived from them, and the benchmark scripts / CI resolve the fixture +# through `make -s print-real-block-fixture` rather than spelling it out. +# To adopt mainnet 25453112 (measured ~110M cycles, ~25% CHEAPER to prove than +# Hoodi's 147.5M): set NETWORK to `mainnet`, the number to 25453112, and SHA256 to +# 0298663d33ae635b5e76266b54ce0f778388c7455e19be3d5207528092b2284f β€” then upload +# that .bin, point the URL at it, and update REAL_BLOCK_FIXTURE in +# tooling/ethrex-tests. The converter's own pins stay on Hoodi and do not move; see +# tooling/ethrex-real-block/README.md. ETHREX_REAL_BLOCK_NETWORK := hoodi ETHREX_REAL_BLOCK := 1265656 -# Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would -# let the benchmark's input change under a fixed fixture name, silently breaking -# comparability across runs. Re-pin deliberately when adopting a new block. -ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142 +# TBD β€” the artifact is not hosted yet. Fill in with the upload location (either +# lambda.alignedlayer.com alongside the sysroot, or a GitHub Release asset). +# Deliberately empty rather than a guessed URL: an unset value fails with an +# actionable message, a wrong one fails with a 404 nobody can act on. +ETHREX_REAL_BLOCK_FIXTURE_URL := ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) -ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_REAL_BLOCK_ID).json ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin -# The cache filename is keyed on the block number only, and its download rule has -# no other prerequisite, so make would treat an already-present cache as up to -# date across an `ETHREX_REPLAY_REV` bump and silently keep converting the old -# input. Depending on a rev-stamped marker makes a re-pin discard the stale cache; -# without it the mismatch only surfaces downstream as a `conversion_is_reproducible` -# digest failure, which reads as "regenerate the fixture" and points at the wrong -# thing. CI never hits this (fresh checkout, `caches/` gitignored) β€” local re-pins do. -ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) @@ -315,6 +320,74 @@ ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) print-real-block-fixture: @echo $(ETHREX_REAL_BLOCK_FIXTURE) +# Lets CI ask "is the fixture hosted yet?" without parsing the Makefile. Prints +# nothing while the URL is unset, which is the condition callers branch on. +print-real-block-fixture-url: + @echo $(ETHREX_REAL_BLOCK_FIXTURE_URL) + +# Download to a temp file and verify BEFORE moving into place, so an interrupted or +# corrupted transfer cannot leave a file that later reads as a valid fixture and +# silently changes what every benchmark measures. Checksum logic mirrors +# prepare-sysroot, including the "neither sha256sum nor shasum" hard error β€” a +# skipped check would defeat the point of fetching a binary. +$(ETHREX_REAL_BLOCK_FIXTURE): + @set -e; \ + if [ -z "$(ETHREX_REAL_BLOCK_FIXTURE_URL)" ]; then \ + echo "ethrex-real-block-fixture: ETHREX_REAL_BLOCK_FIXTURE_URL is unset." >&2; \ + echo " The $(ETHREX_REAL_BLOCK_ID) fixture is fetched, not built. Set the URL in the" >&2; \ + echo " Makefile to wherever the .bin is hosted, or regenerate locally with:" >&2; \ + echo " make regen-real-block-fixture" >&2; \ + exit 1; \ + fi; \ + mkdir -p $(dir $@); \ + tmp="$@.tmp"; \ + cleanup() { rm -f "$$tmp"; }; \ + trap 'cleanup' EXIT; \ + trap 'cleanup; exit 130' INT; \ + trap 'cleanup; exit 143' TERM; \ + echo "Downloading real-block fixture $(ETHREX_REAL_BLOCK_ID)..."; \ + curl -fL --proto '=https' --retry 3 --retry-delay 2 --retry-all-errors \ + "$(ETHREX_REAL_BLOCK_FIXTURE_URL)" -o "$$tmp"; \ + echo "Verifying fixture checksum..."; \ + checksum_ok=false; \ + if command -v sha256sum >/dev/null 2>&1; then \ + printf '%s %s\n' "$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)" "$$tmp" | sha256sum -c - >/dev/null && checksum_ok=true; \ + elif command -v shasum >/dev/null 2>&1; then \ + actual="$$(shasum -a 256 "$$tmp" | awk '{print $$1}')"; \ + [ "$$actual" = "$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)" ] && checksum_ok=true; \ + else \ + echo "ethrex-real-block-fixture: missing sha256sum or shasum for checksum verification" >&2; \ + exit 1; \ + fi; \ + if [ "$$checksum_ok" != true ]; then \ + echo "ethrex-real-block-fixture: checksum mismatch for $(ETHREX_REAL_BLOCK_FIXTURE_URL)" >&2; \ + exit 1; \ + fi; \ + mv "$$tmp" "$@"; \ + trap - EXIT + +# ===== Real-block converter (regeneration tool, off the build path) ===== +# +# Only needed when the guest's ethrex `rev` moves and the fixture has to be rebuilt, +# or when validating a candidate block. Nothing in the benchmark or test path builds +# this crate any more. +# +# The cache stays pinned by immutable `rev`, as the guest pins ethrex itself: a +# branch ref would let the converter's reproducibility digest drift under a fixed +# input. These pins are Hoodi-only on purpose β€” that is the one cache ethrex-replay +# hosts, and it is what `conversion_is_reproducible` reads. They do NOT move when +# the benchmark block above is repointed; the two are now independent. +ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 +ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656 +ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json +# The cache filename is keyed on the block only, and its download rule has no other +# prerequisite, so make would treat an already-present cache as up to date across an +# `ETHREX_REPLAY_REV` bump and silently keep reading the old input. Depending on a +# rev-stamped marker makes a re-pin discard the stale cache; without it the mismatch +# only surfaces downstream as a `conversion_is_reproducible` digest failure, which +# reads as "regenerate the fixture" and points at the wrong thing. +ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) + $(ETHREX_REPLAY_REV_STAMP): mkdir -p $(dir $@) rm -f $(ETHREX_REAL_BLOCK_CACHE) $(dir $@).replay-rev-* @@ -323,22 +396,35 @@ $(ETHREX_REPLAY_REV_STAMP): $(ETHREX_REAL_BLOCK_CACHE): $(ETHREX_REPLAY_REV_STAMP) mkdir -p $(dir $@) curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ - https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_REAL_BLOCK_ID).json + https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json mv $@.tmp $@ -$(ETHREX_REAL_BLOCK_FIXTURE): $(ETHREX_REAL_BLOCK_CACHE) tooling/ethrex-real-block/src/main.rs tooling/ethrex-real-block/Cargo.toml tooling/ethrex-real-block/Cargo.lock +# Converter correctness: host-side parity through the guest's own Crypto impl, the +# network-rejection guard, and the reproducibility digest. Runs on changes to the +# converter (see .github/workflows/ethrex-real-block.yml), not on every PR. +test-ethrex-real-block-converter: $(ETHREX_REAL_BLOCK_CACHE) + cd tooling/ethrex-real-block && cargo test --release + +# Manual regeneration. Overwrites the fetched fixture in place so you can hash the +# result and upload it β€” that upload, plus SHA256/URL above, is how the fixture is +# actually replaced. Only rebuilds the Hoodi block: a different block needs its own +# ethrex-replay cache, which upstream does not host (see the crate README). +regen-real-block-fixture: $(ETHREX_REAL_BLOCK_CACHE) cd tooling/ethrex-real-block && \ - cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$@ + cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock). +# Needs the real-block fixture, so it needs the fixture URL to be set; CI runs the +# `-offline` variant below in the PR gate and this one only in ethrex-real-block.yml. test-ethrex: compile-programs-rust $(ETHREX_REAL_BLOCK_FIXTURE) cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm -# Offline variant: no network. `--skip test_ethrex_real_block` is a substring -# match, so it drops both real-block tests β€” the `_vm` one and the `_native` one, -# which reads the downloaded fixture and would otherwise fail on a clean checkout. -# The committed synthetic fixtures and `no_kzg_backend_linked` still run. +# Offline variant: no network, and what the PR gate runs. `--skip +# test_ethrex_real_block` is a substring match, so it drops both real-block tests β€” +# the `_vm` one and the `_native` one, which reads the fetched fixture and would +# otherwise fail on a clean checkout. The committed synthetic fixtures and +# `no_kzg_backend_linked` still run. test-ethrex-offline: compile-programs-rust cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block diff --git a/bench_vs/run_ethrex.sh b/bench_vs/run_ethrex.sh index 4438e1c26..b323d5866 100755 --- a/bench_vs/run_ethrex.sh +++ b/bench_vs/run_ethrex.sh @@ -33,10 +33,12 @@ NC='\033[0m' BLOCKS=( "ethrex empty block|ethrex_empty_block.bin" "ethrex 1 tx|ethrex_simple_tx.bin" - # ethrex_10_transfers.bin (~42M cycles) executes fine but is too heavy to - # prove on a typical machine (OOMs ~36 GB) β€” software ecrecover dominates - # (~4M cycles/transfer). Kept as a fixture; add here once ecrecover is a - # precompile or for big-memory/nightly proving. + # ethrex_10_transfers.bin is 6.8M cycles (measured), NOT the ~42M this comment + # used to claim: that figure β€” and the "OOMs ~36 GB, software ecrecover + # dominates at ~4M cycles/transfer" reasoning built on it β€” predates ecrecover + # becoming an ECSM accelerator, which cut it ~6x. It is no longer too heavy to + # prove. Left out only because the two blocks above already cover the cheap + # end; add it if a mid-size point is wanted. ) # --- Parse args ------------------------------------------------------------- diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index e8f1467ad..a151593d9 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -17,16 +17,21 @@ # # Workload. What this script measures is VERIFY cost, and verify cost is structural # in the proof β€” table mix, trace lengths, query count β€” so it is the block that -# decides what is being measured, not the loop around it. The synthetic 20-transfer -# default is 20 plain transfers: ecrecover-heavy, near-empty state (measured: 14.2M -# cycles, 411 keccak calls, 80 ecsm calls). A real block inverts that mix (168.3M -# cycles, 9,046 keccak, 44 ecsm), so a verifier change can move the two differently. +# decides what is being measured, not the loop around it. The synthetic default is 20 +# plain transfers: ecrecover-heavy, near-empty state (9.06M cycles, 411 keccak calls, +# 80 ecsm calls). A real block inverts that mix (147.5M cycles, 9,046 keccak, 44 +# ecsm), so a verifier change can move the two differently. Both counts are for a +# current guest ELF; they shift ~14% with ELF vintage, so pin the ELF when quoting one. # # WORKLOAD=real is therefore the honest number and WORKLOAD=synthetic the cheap one; # the default is cheap because the real block must be proven with continuations -# (~15 min per side, once, then cached in $WORK) before any verify run happens. +# (~13 min per side, once, then cached in $WORK) before any verify run happens. # Both sides always use the same block, so a comparison is never mixed. # +# Disk: a real-block continuation bundle is ~5 GB and this script CACHES both sides' +# proofs in $WORK, so budget ~10 GB there. The fixture itself is fetched by URL + +# sha256 (see the Makefile); while that URL is unset, WORKLOAD=real cannot run. +# # `/bench-verify` runs the synthetic default: that job already budgets 90 minutes # for the verifier bench plus a 70-minute recursion step, and two real-block proves # do not fit under the cap. Run WORKLOAD=real by hand on the bench server. @@ -96,9 +101,9 @@ if [ ! -f "$ELF_REL" ]; then fi if [ ! -f "$INPUT_REL" ]; then if [ "$WORKLOAD" = "real" ]; then - # ~1 MB and gitignored, so it is never in a fresh checkout. Cold runs also - # download a ~1.5 MB ethrex-replay cache (pinned rev, so the input can't drift). - echo "==> Generating ethrex real-block fixture (missing; downloads a cache on a cold run)" + # ~1 MB, gitignored, never in a fresh checkout. Fetched by URL + sha256, not + # built: no converter and no ethrex host dependency tree on this path. + echo "==> Fetching ethrex real-block fixture (missing)" make ethrex-real-block-fixture else echo "==> Generating ethrex 20-transfer fixture (missing)" diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 41eb80d33..da03694a7 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -17,12 +17,14 @@ # EPOCH_SIZE_LOG2= (default 21) sizes the epoch, WORKLOAD=real only. # # Pick the workload that matches the run you are localizing, because the symbol -# mix follows the block: the synthetic default is 20 plain transfers (measured: -# 14.2M cycles, 411 keccak calls, 80 ecsm calls) and a real block inverts that -# (168.3M cycles, 9,046 keccak, 44 ecsm), so a hot symbol in one need not be hot -# in the other. WORKLOAD=real also switches to a continuation prove (monolithic -# would need ~500 GB at that trace length), which is ~15 min per recording β€” five -# recordings, so budget over an hour. +# mix follows the block: the synthetic default is 20 plain transfers (9.06M cycles, +# 411 keccak calls, 80 ecsm calls) and a real block inverts that (147.5M cycles, +# 9,046 keccak, 44 ecsm), so a hot symbol in one need not be hot in the other. +# Both counts are for a current guest ELF; they shift ~14% with ELF vintage. +# +# WORKLOAD=real also switches to a continuation prove (monolithic would need +# ~700 GB at that trace length), which is ~13 min per recording β€” five recordings, +# so budget over an hour, plus ~5 GB of disk per bundle. # # Produces: # - two perf-diff tables (recorded twice per side, interleaved B A B A β€” @@ -69,9 +71,9 @@ command -v perf >/dev/null 2>&1 || { echo "ERROR: perf not installed (linux-tool [ -f "$ELF_REL" ] || { echo "ERROR: missing $ELF_REL β€” run bench_abba.sh once (it builds the guest)." >&2; exit 1; } if [ ! -f "$INPUT_REL" ]; then if [ "$WORKLOAD" = "real" ]; then - # ~1 MB and gitignored, so it is never in a fresh checkout; generate rather than - # abort. A cold run also downloads a ~1.5 MB ethrex-replay cache (pinned rev). - echo "==> Generating ethrex real-block fixture (missing; downloads a cache on a cold run)" + # ~1 MB, gitignored, never in a fresh checkout; fetch rather than abort. This is + # a URL + sha256 download, not a build. + echo "==> Fetching ethrex real-block fixture (missing)" make ethrex-real-block-fixture else echo "ERROR: missing $INPUT_REL β€” run bench_abba.sh once (it builds the fixture)." >&2 diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index dd6e589e3..44fc02b7d 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -18,9 +18,14 @@ opposite β€” a block that actually looks like Ethereum. | state trie | 40 accounts, fresh genesis | 1,705 nodes | | storage keys | 0 | 422 | | serialized size | 32,766 B | 1,021,207 B | +| cycles | 9,063,727 | **147,482,439** | +| keccak / ecsm calls | 411 / 80 | **9,046** / 44 | -Note it has *fewer* transactions than the synthetic fixture while being far more -representative β€” transaction count is not the axis that matters. +Note it has *fewer* transactions than the synthetic fixture while being ~16x the +work β€” transaction count is not the axis that matters. The crypto mix inverts too: +the synthetic block runs ~5 keccaks per ecrecover, the real one ~206. Cycle counts +are for a current guest ELF and move ~14% with ELF vintage; pin the ELF when quoting +one. The synthetic column is `ethrex_bench_20.bin` as the benchmark scripts actually generate it β€” `ethrex-fixtures 20 … distinct`, i.e. 20 distinct genesis-funded @@ -33,27 +38,46 @@ Block 1265656 is **verified to run on the guest's precompile surface** (see replacement block must clear the same check β€” that is what makes it usable, not just realistic. -## Prerequisites -Rust (stable) and network access on first run (cargo fetches the pinned ethrex -crates; `make` downloads the cache). **No RV64 target or sysroot needed** β€” this -is a host tool. +## Getting the fixture -## How to run - -From the repo root, the committed default (Hoodi block 1265656): +The fixture is **fetched, not built**: ```bash make ethrex-real-block-fixture ``` -That downloads the cache to `caches/` (gitignored) and writes -`executor/tests/ethrex_hoodi_1265656.bin` (gitignored β€” ~1 MB, too large to -commit; see `executor/.gitignore`). +That downloads the finished `.bin` from `ETHREX_REAL_BLOCK_FIXTURE_URL` and +verifies `ETHREX_REAL_BLOCK_FIXTURE_SHA256` before moving it into place β€” +the same contract as `prepare-sysroot`. The file is gitignored (~1 MB; see +`executor/.gitignore`), and a corrupt or interrupted download is discarded rather +than left looking valid. + +> The URL is currently **unset**. Until the artifact is hosted, this target fails +> with an actionable message and every consumer degrades gracefully: the benchmark +> workflow warns and skips its real-block section, and the usability screen in +> `.github/workflows/ethrex-real-block.yml` skips too. + +This crate is *not* on that path. Fetching a verified binary is what takes the +converter, the ~335-package ethrex host dependency tree, the ethrex-replay cache +and the `rev` pin off the critical path of everyone who just wants to run a +benchmark. It also decouples the block from what upstream hosts: ethrex-replay +publishes a cache for Hoodi and nothing else, so a mainnet block is unreachable by +the convert-locally route and trivial by this one. -The cache URL is pinned to an ethrex-replay **commit**, not to `main` -(`ETHREX_REPLAY_REV` in the Makefile), matching how the guest pins ethrex. A -branch ref would let the benchmark's input change under a fixed fixture name, -which would quietly destroy comparability between runs. +## Regenerating it (ethrex rev bumps) + +Needed roughly twice a year, when the guest's ethrex `rev` moves and the rkyv +layout changes with it: + +```bash +make regen-real-block-fixture # downloads the pinned cache, rebuilds the .bin +sha256sum executor/tests/ethrex_hoodi_1265656.bin +``` + +Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_URL` / +`_SHA256`. The cache URL is pinned to an ethrex-replay **commit**, not `main` +(`ETHREX_REPLAY_REV`), matching how the guest pins ethrex β€” a branch ref would let +the derivation drift under a fixed input. Directly, against any cache file: @@ -79,27 +103,55 @@ asserts it. The pinned commit keeps the *input* immutable; the digest keeps the | Where | How to run it | Cost | |---|---|---| -| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~15 min | -| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~15 min per side, then cached | +| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~13 min | +| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~13 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so >1 h | None of them hardcode the fixture path β€” they read it from `make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when -the (gitignored) `.bin` is absent. +the `.bin` is absent. **Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic -prove grows ~3.1 GB per million cycles on this workload family (measured on the -bench server: 2007 MB per transfer at RΒ² = 0.998 across 4β†’20 transfers), and the -real block is 168.3M cycles β€” roughly 500 GB. `--continuations` makes peak heap a -function of the epoch size instead of the trace length, so the same block fits in -~16 GB at epoch 2^21. +prove grows ~4.9 GB per million cycles on this workload family (measured on the +bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), +and the real block is 147.5M cycles β€” roughly **700 GB**. `--continuations` makes +peak heap a function of the epoch size instead of the trace length, so the same +block fits in **~21 GB** at epoch 2^21: a 15.8 GB epoch working set plus ~69 MB per +epoch of accumulated proofs over 68 epochs. The bundle on disk is ~5 GB, and +serializing it past 2 GiB needs rkyv `pointer_width_64`. Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in -~25s against ~15 min, and the bench runner is single and serialized across all PRs. -The synthetic number is a fast screen; the real block is the number that means -something. +~25s against ~13 min, and one runner carries every `/bench`, `/bench-abba` and +`/bench-verify` in the repo. The synthetic number is a fast screen; the real block +is the number that means something. + +Cycle counts here (9.06M synthetic, 147.5M real) are for a **current guest ELF**. +They move ~14% with ELF vintage β€” the same Hoodi block reads 168.3M on a +mid-July ELF β€” so pin the ELF whenever you quote one, or it will look like a +regression the next time someone measures. -## Getting a cache for a different block +## Where validation runs + +The checks themselves are described under [Validation](#validation) below; this is +where each one executes. + +`.github/workflows/ethrex-real-block.yml` runs them on changes to this crate, +`tooling/ethrex-tests`, or the `Makefile` β€” **not** on every PR. The fixture is a +benchmark input, read by no product code; it has to be right when it changes, not on +every commit, and running it per-PR put a network fetch and a cold build of ~335 +packages in the required gate. + +`no_kzg_backend_linked` is the exception and stays in the required gate +(`pr_main.yaml`): it is a pure unit test costing microseconds, and it is the property +the usability screen depends on, so it should fail on the PR that breaks it rather +than on some later, unrelated one. + +## Prerequisites (for regeneration only) +Rust (stable) and network access on first run (cargo fetches the pinned ethrex +crates; `make` downloads the cache). **No RV64 target or sysroot needed** β€” this +is a host tool. + +## Getting a cache for a different block (regeneration only) The cache format is `ethrex-replay`'s, so use that tool to produce one β€” it handles the RPC fetching, multiple client backends, and the `eth_getProof` @@ -121,32 +173,41 @@ the block under invented rules while still passing every check here, since the witness is only ever validated against whichever config we chose. The converter refuses instead; `unmappable_network_is_rejected` pins that. -Then point this tool at the resulting JSON. Adopting a different block as the -default is an edit, not a flag. Three of the four edits are the block's identity; -the fourth is the digest that proves the conversion still matches. +## Adopting a different block + +Since the fixture is fetched rather than converted locally, the **benchmark block +and this crate's test block are independent**. This crate stays pinned to Hoodi +1265656 β€” that is the one cache ethrex-replay hosts, and `conversion_is_reproducible` +reads it β€” and none of its pins move when the benchmark block changes. -**1. The Makefile β€” this is what moves every benchmark.** Two lines: +Swapping the benchmark block is **four lines**, once the new `.bin` is hosted: ```make -ETHREX_REAL_BLOCK_NETWORK := hoodi # -> mainnet -ETHREX_REAL_BLOCK := 1265656 # -> 25453112 +# Makefile +ETHREX_REAL_BLOCK_NETWORK := hoodi # -> mainnet +ETHREX_REAL_BLOCK := 1265656 # -> 25453112 +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 1f7d4c… # -> 0298663d… +ETHREX_REAL_BLOCK_FIXTURE_URL := … # -> the new artifact ``` -Everything downstream is derived from them: the cache path, the download URL, the -fixture name, and β€” through `make -s print-real-block-fixture` β€” the benchmark -scripts and `benchmark-pr.yml`. Nothing else names the block. Re-pin -`ETHREX_REPLAY_REV` in the same edit if the new cache landed in a later commit. - -**2. `src/main.rs`** β€” `CACHE` and the block assertions in -`conversion_is_reproducible`, including the **sha256**. The digest cannot be -guessed: run `make ethrex-real-block-fixture`, take the printed `sha256:` line, -and paste it in. That is the check that keeps the derivation honest. - -**3. `tooling/ethrex-tests`** β€” `REAL_BLOCK_FIXTURE`. - -**4. Nothing in `executor/.gitignore`** β€” it already ignores every accepted -network's fixture name, so a repointed ~1 MB fixture cannot become committable by -accident. +plus `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests` (which points the usability +screen at the block actually being benchmarked). Everything else derives from the +Makefile β€” fixture name, and through `make -s print-real-block-fixture` the +benchmark scripts and `benchmark-pr.yml`. Nothing in `executor/.gitignore` needs +touching: it already ignores every accepted network's fixture name, so a repointed +~1 MB fixture cannot become committable by accident. + +**Mainnet 25453112 is ready to adopt.** It passes the usability screen (stateless +re-execution reaches a matching post-state root with the KZG screen live), its +digest is `0298663d33ae635b5e76266b54ce0f778388c7455e19be3d5207528092b2284f`, and +at ~110M cycles it is ~25% *cheaper* to prove than Hoodi's 147.5M β€” roughly 10 min +rather than 13. It is 1.93 MiB (3% of the 64 MiB private-input clamp). The only +thing missing is a host for the `.bin`. + +Note it cannot be produced by `make regen-real-block-fixture`: that route needs an +ethrex-replay cache, upstream hosts only Hoodi's, and generating a mainnet one takes +~4 minutes and ~700 calls against an archive RPC. This is precisely why the fixture +is fetched rather than converted. Benchmark comparability does not survive the swap, and that is intentional rather than a wrinkle to work around: `benchmark-pr.yml` records which block it measured @@ -186,9 +247,10 @@ milliseconds. What costs time is a cold build of the ethrex host dependency tree β€” ~335 packages, including the `blst`, `c-kzg` and `secp256k1-sys` C builds, `malachite` -and `ark-ff/asm` β€” not the tests. CI caches this workspace's `target/` (see the -`workspaces:` list on the `test-executor` job's `rust-cache` step), so only cold -runs pay it. +and `ark-ff/asm` β€” not the tests. That build is why these checks live in their own +path-filtered workflow rather than the PR gate (see [Where validation +runs](#where-validation-runs)); `ethrex-real-block.yml` caches this workspace's +`target/` under its own key, so only cold runs pay it. **`cargo test` here β€” `real_block_executes_under_guest_crypto`.** Executes the block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is From 2e0297cd94eae4b83ba074204807ea52d2ae6b54 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:04:30 -0300 Subject: [PATCH 12/27] docs(ethrex): keep the block choice out of the wiring, and record measured costs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the fetch change. Two things. No block number appears outside the Makefile's four variables. Comments in benchmark-pr.yml and this README previously named a specific candidate block as the destination of a repoint, which would have gone stale the moment a different candidate won and read as if the wiring depended on it. Every figure that is a property of the block is now labelled "current default", and the repoint procedure shows placeholders rather than one candidate's values. Verified: no workflow, script or env var names a block. Recorded the measured cost of each candidate, since prove time is a property of the block and moves with the repoint. mainnet 25453112 is 125,932,956 cycles on the mid-July ELF against hoodi 1265656's 168,319,360 on the same ELF β€” ~110M vs 147.5M on a current one, so ~10 min and ~19 GB rather than ~13 min and ~21 GB. Cheaper despite the larger fixture: cycles per gas is not constant across blocks, so the earlier gas-ratio estimate for it was wrong. Costs are stated as ranges where they span the candidates. --- .github/workflows/benchmark-pr.yml | 29 ++++++----- Makefile | 24 ++++++---- tooling/ethrex-real-block/README.md | 74 ++++++++++++++++++----------- 3 files changed, 77 insertions(+), 50 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 81702d78a..cc3503495 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -54,23 +54,26 @@ env: # will read as a regression β€” always pin the ELF when repeating one. ELF: executor/program_artifacts/rust/ethrex.elf INPUT: executor/tests/ethrex_bench_20.bin - # Headline (representative) workload: a real Ethereum block. 147.5M cycles, 9,046 - # keccak calls, 44 ecsm calls β€” ~16x the work of the synthetic block with a ~40x - # different keccak:ecrecover mix. That is the whole point: a prover change can move - # the synthetic number and the real one in opposite directions. + # Headline (representative) workload: a real Ethereum block. WHICH block lives in + # the Makefile and nowhere else β€” nothing in this file names one, so a repoint + # moves this job without editing it. At the current default that is 147.5M cycles, + # 9,046 keccak calls and 44 ecsm calls: ~16x the work of the synthetic block with a + # ~40x different keccak:ecrecover mix. That is the whole point β€” a prover change + # can move the synthetic number and the real one in opposite directions. # - # The path is NOT hardcoded here: it is resolved from the Makefile - # (`make -s print-real-block-fixture`) into REAL_INPUT at run time, so repointing - # the block is a Makefile edit and nothing else. The ~1 MB .bin is gitignored and - # FETCHED by URL + sha256 (see "Fetch ethrex real-block fixture"); while that URL - # is unset the whole section degrades to a warning rather than failing the job. + # The path is resolved from the Makefile (`make -s print-real-block-fixture`) into + # REAL_INPUT at run time. The ~1 MB .bin is gitignored and FETCHED by URL + sha256 + # (see "Fetch ethrex real-block fixture"); while that URL is unset the whole + # section degrades to a warning rather than failing the job. # # Continuations are mandatory here, not a preference: a monolithic prove costs # ~4.9 GB of peak heap per million cycles on this workload family (from the - # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so 147.5M - # cycles would need ~700 GB. `--continuations` makes peak heap a function of the - # epoch size instead of the trace length: ~21 GB here (a 15.8 GB epoch-2^21 - # working set plus ~69 MB per epoch of accumulated proofs over 68 epochs). + # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so a real + # block in the 110-150M cycle range would need 500-700 GB. `--continuations` makes + # peak heap a function of the epoch size instead of the trace length: ~19-21 GB + # here (a 15.8 GB epoch-2^21 working set plus ~69 MB per epoch of accumulated + # proofs). Costs move with the block; the per-candidate table is in + # tooling/ethrex-real-block/README.md. # # Budget ~5 GB of disk for the bundle each run β€” hence the `rm -f` after every # prove. Serializing it past 2 GiB needs rkyv `pointer_width_64`, so a PR branch diff --git a/Makefile b/Makefile index c04b36edb..24e930ae5 100644 --- a/Makefile +++ b/Makefile @@ -275,7 +275,7 @@ test-rust: compile-programs-rust # ===== Real-block benchmark fixture ===== # -# A genuine Ethereum block (Hoodi 1265656: 4.4M gas, 11 txs, ~124 KB of contract +# A genuine Ethereum block (currently Hoodi 1265656: 4.4M gas, 11 txs, ~124 KB of contract # bytecode, 1705 state-trie nodes), as opposed to the synthetic N-plain-transfer # blocks from tooling/ethrex-fixtures. ~1 MB, gitignored. # @@ -291,15 +291,21 @@ test-rust: compile-programs-rust # It is now a regeneration tool (ethrex rev bumps, roughly twice a year), not a # build step. # -# Which block the benchmarks run is these THREE lines and nothing else. Every path +# Which block the benchmarks run is these FOUR lines and nothing else. Every path # below is derived from them, and the benchmark scripts / CI resolve the fixture -# through `make -s print-real-block-fixture` rather than spelling it out. -# To adopt mainnet 25453112 (measured ~110M cycles, ~25% CHEAPER to prove than -# Hoodi's 147.5M): set NETWORK to `mainnet`, the number to 25453112, and SHA256 to -# 0298663d33ae635b5e76266b54ce0f778388c7455e19be3d5207528092b2284f β€” then upload -# that .bin, point the URL at it, and update REAL_BLOCK_FIXTURE in -# tooling/ethrex-tests. The converter's own pins stay on Hoodi and do not move; see -# tooling/ethrex-real-block/README.md. +# through `make -s print-real-block-fixture` rather than spelling it out. No +# workflow, script or test outside this block names a block number. +# +# Repointing: set NETWORK and the block number, upload that .bin somewhere and put +# its location and sha256 in the two lines below, then update REAL_BLOCK_FIXTURE in +# tooling/ethrex-tests so the usability screen follows. Nothing else moves β€” in +# particular the converter's own pins stay on Hoodi, because that is the one +# ethrex-replay cache upstream hosts. Full procedure and the measured cost of each +# candidate block: tooling/ethrex-real-block/README.md. +# +# Hoodi 1265656 is the current default because it is the block whose fixture and +# digest already exist, not because it is the best candidate; the mainnet blocks +# under evaluation are cheaper to prove. ETHREX_REAL_BLOCK_NETWORK := hoodi ETHREX_REAL_BLOCK := 1265656 ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142 diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 44fc02b7d..edc78d2c6 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -9,7 +9,10 @@ they execute no contract code, touch a genesis state trie only a couple of levels deep, and carry no bytecode in the witness. This tool produces the opposite β€” a block that actually looks like Ethereum. -| | `ethrex_bench_20.bin` (synthetic) | `ethrex_hoodi_1265656.bin` (real) | +Figures below are for the **current default** real block; a repoint replaces them +(see [Adopting a different block](#adopting-a-different-block)). + +| | `ethrex_bench_20.bin` (synthetic) | `ethrex_hoodi_1265656.bin` (real, default) | |---|---|---| | gas used | 420,000 | **4,402,947** | | transactions | 20 (all plain transfers) | 11 (7Γ— EIP-1559, 4Γ— EIP-4844 blob) | @@ -101,23 +104,26 @@ asserts it. The pinned commit keeps the *input* immutable; the digest keeps the ## What benchmarks with it -| Where | How to run it | Cost | +Costs below are for the **current default block** (hoodi 1265656) and move with it β€” +see [Measured cost of candidate blocks](#measured-cost-of-candidate-blocks). + +| Where | How to run it | Cost (current default) | |---|---|---| | `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~13 min | | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~13 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so >1 h | -None of them hardcode the fixture path β€” they read it from +None of them hardcode the fixture path or a block number β€” they read the path from `make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when the `.bin` is absent. **Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic prove grows ~4.9 GB per million cycles on this workload family (measured on the bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), -and the real block is 147.5M cycles β€” roughly **700 GB**. `--continuations` makes -peak heap a function of the epoch size instead of the trace length, so the same -block fits in **~21 GB** at epoch 2^21: a 15.8 GB epoch working set plus ~69 MB per -epoch of accumulated proofs over 68 epochs. The bundle on disk is ~5 GB, and +so a real block in the 110–150M cycle range needs **500–700 GB**. `--continuations` +makes peak heap a function of the epoch size instead of the trace length, so the +same block fits in **~19–21 GB** at epoch 2^21: a 15.8 GB epoch working set plus +~69 MB per epoch of accumulated proofs. The bundle on disk is ~4–5 GB, and serializing it past 2 GiB needs rkyv `pointer_width_64`. Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in @@ -183,31 +189,43 @@ reads it β€” and none of its pins move when the benchmark block changes. Swapping the benchmark block is **four lines**, once the new `.bin` is hosted: ```make -# Makefile -ETHREX_REAL_BLOCK_NETWORK := hoodi # -> mainnet -ETHREX_REAL_BLOCK := 1265656 # -> 25453112 -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 1f7d4c… # -> 0298663d… -ETHREX_REAL_BLOCK_FIXTURE_URL := … # -> the new artifact +# Makefile β€” the ONLY place a block number appears +ETHREX_REAL_BLOCK_NETWORK := +ETHREX_REAL_BLOCK := +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := +ETHREX_REAL_BLOCK_FIXTURE_URL := ``` plus `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests` (which points the usability screen at the block actually being benchmarked). Everything else derives from the -Makefile β€” fixture name, and through `make -s print-real-block-fixture` the -benchmark scripts and `benchmark-pr.yml`. Nothing in `executor/.gitignore` needs -touching: it already ignores every accepted network's fixture name, so a repointed -~1 MB fixture cannot become committable by accident. - -**Mainnet 25453112 is ready to adopt.** It passes the usability screen (stateless -re-execution reaches a matching post-state root with the KZG screen live), its -digest is `0298663d33ae635b5e76266b54ce0f778388c7455e19be3d5207528092b2284f`, and -at ~110M cycles it is ~25% *cheaper* to prove than Hoodi's 147.5M β€” roughly 10 min -rather than 13. It is 1.93 MiB (3% of the 64 MiB private-input clamp). The only -thing missing is a host for the `.bin`. - -Note it cannot be produced by `make regen-real-block-fixture`: that route needs an -ethrex-replay cache, upstream hosts only Hoodi's, and generating a mainnet one takes -~4 minutes and ~700 calls against an archive RPC. This is precisely why the fixture -is fetched rather than converted. +Makefile β€” the fixture name, and through `make -s print-real-block-fixture` the +benchmark scripts and `benchmark-pr.yml`. No workflow, script or env var names a +block. Nothing in `executor/.gitignore` needs touching either: it already ignores +every accepted network's fixture name, so a repointed ~1 MB fixture cannot become +committable by accident. + +### Measured cost of candidate blocks + +Cost is a property of the block, so it changes with the repoint. Cycles are given +for a **current guest ELF**; prove time and heap are for the CPU bench runner at +epoch 2^21, using the 5.31–5.62 s per Mcycle measured on that box. + +| block | cycles | prove | peak heap | fixture | +|---|---|---|---|---| +| hoodi 1265656 β€” *current default* | 147.5M | ~13 min | ~21 GB | 1,021,207 B, `1f7d4c4c…` | +| mainnet 25453112 | ~110M | ~10 min | ~19 GB | 2,019,747 B, `0298663d…` | + +These are measurements, not estimates: the mainnet block executes in 125,932,956 +cycles on the mid-July ELF against Hoodi's 168,319,360 on the same ELF, i.e. ~25% +**cheaper** despite being the larger fixture β€” cycles per gas is not constant across +blocks. Both clear the usability screen. Further candidates are being measured; add +a row rather than editing the wiring. + +Hoodi is the default only because its fixture and digest already exist. Note it is +also the only block `make regen-real-block-fixture` can rebuild: that route needs an +ethrex-replay cache, upstream hosts Hoodi's alone, and producing another takes ~4 +minutes and ~700 calls against an archive RPC. That asymmetry is exactly why the +fixture is fetched rather than converted. Benchmark comparability does not survive the swap, and that is intentional rather than a wrinkle to work around: `benchmark-pr.yml` records which block it measured From 31fb04159d0bdad5780a80ca111b0d260bbe522c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:17:52 -0300 Subject: [PATCH 13/27] bench(ethrex): repoint the real block to mainnet 25368371 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Executes the documented repoint now that block selection is final. The chosen block was the only one in a 90-day Dune sweep matching the shape constraints (exactly 2 heavy txs, no whale tx, sane plain-transfer share) in the 1.6-2.6M gas band, so it is structurally typical rather than merely large. It is also the cheapest candidate measured, which matters because this workload sits on a single shared bench runner: ~65.6M cycles on a current guest ELF (74,819,518 on the mid-July one) against hoodi 1265656's 147.5M, i.e. ~6 min per prove rather than ~13, ~18 GB peak and a ~1.9 GB bundle. Note cycles/gas is not constant across blocks (~26-38 here), so ranking candidates by gas would have picked wrong: 25453112 carries MORE gas than the hoodi block yet costs ~25% fewer cycles. Wiring: unchanged. Nothing outside the Makefile's variables names a block, so the repoint is those variables plus the test constants that prove it self-consistent. Verified by grep that no workflow, script or env var mentions a block number. The cache is now fetched and verified like the fixture rather than pulled from raw.githubusercontent.com at a pinned ethrex-replay rev. That route only ever worked for the one block upstream publishes, which is what previously forced the converter's tests onto a different block from the benchmarks. Hosting both artifacts collapses that split: conversion_is_reproducible now pins the SAME block the benchmarks prove, so its digest assert covers the artifact in use. ETHREX_REPLAY_REV and its stamp are gone with it β€” this supersedes 05e02851, whose staleness guard is carried forward in a stronger form below. Fetch targets are now phony with an internal guard (the prepare-sysroot shape) instead of file rules. A file rule does not run when its target exists, so it could never catch the case that matters: an artifact already on disk that is stale after a re-upload under the same block number. The digest is now re-checked on every invocation, which also catches corruption and hand-placed copies. Validation, all run: - make test-ethrex-real-block-converter: 3/3 pass against the new constants. conversion_is_reproducible passing is the self-consistency proof β€” the cache, the block stats, MAINNET_CHAIN_ID and the digest all describe one block. - regenerating from the cache reproduces the fixture byte for byte (61eba49b...), so the handed-over .bin and the hosted cache agree. - test_ethrex_real_block_native and no_kzg_backend_linked pass: the block clears the guest's precompile surface with the KZG screen live. - fetch guard exercised: matching digest is a silent no-op, a tampered file is detected and reported, and an unset URL fails with an actionable message. Incidental: tooling/ethrex-tests/Cargo.lock gains ecsm's num-integer entry. That manifest has declared it all along and the detached lockfile was stale, so any build regenerates this and a --locked build in that workspace would have failed. --- .github/workflows/benchmark-pr.yml | 32 ++-- Makefile | 218 +++++++++++++------------ scripts/bench_verify.sh | 8 +- scripts/perf_diff.sh | 8 +- tooling/ethrex-real-block/README.md | 219 +++++++++++++++----------- tooling/ethrex-real-block/src/main.rs | 20 +-- tooling/ethrex-tests/Cargo.lock | 1 + tooling/ethrex-tests/tests/ethrex.rs | 2 +- 8 files changed, 270 insertions(+), 238 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index cc3503495..61f7d3903 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -56,9 +56,9 @@ env: INPUT: executor/tests/ethrex_bench_20.bin # Headline (representative) workload: a real Ethereum block. WHICH block lives in # the Makefile and nowhere else β€” nothing in this file names one, so a repoint - # moves this job without editing it. At the current default that is 147.5M cycles, - # 9,046 keccak calls and 44 ecsm calls: ~16x the work of the synthetic block with a - # ~40x different keccak:ecrecover mix. That is the whole point β€” a prover change + # moves this job without editing it. At the current default that is ~65.6M cycles, + # 10,478 keccak calls and 116 ecsm calls: ~7x the work of the synthetic block with a + # ~18x different keccak:ecrecover mix. That is the whole point β€” a prover change # can move the synthetic number and the real one in opposite directions. # # The path is resolved from the Makefile (`make -s print-real-block-fixture`) into @@ -68,16 +68,16 @@ env: # # Continuations are mandatory here, not a preference: a monolithic prove costs # ~4.9 GB of peak heap per million cycles on this workload family (from the - # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so a real - # block in the 110-150M cycle range would need 500-700 GB. `--continuations` makes - # peak heap a function of the epoch size instead of the trace length: ~19-21 GB - # here (a 15.8 GB epoch-2^21 working set plus ~69 MB per epoch of accumulated - # proofs). Costs move with the block; the per-candidate table is in - # tooling/ethrex-real-block/README.md. + # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so the + # current default would need ~330 GB and a heavier candidate up to ~700 GB. + # `--continuations` makes peak heap a function of the epoch size instead of the + # trace length: ~18 GB here (a 15.8 GB epoch-2^21 working set plus ~60 MB per + # epoch of accumulated proofs over ~32 epochs). Costs move with the block; the + # per-candidate table is in tooling/ethrex-real-block/README.md. # - # Budget ~5 GB of disk for the bundle each run β€” hence the `rm -f` after every - # prove. Serializing it past 2 GiB needs rkyv `pointer_width_64`, so a PR branch - # predating that fix fails here at write time rather than mismeasuring. + # Budget ~2 GB of disk for the bundle each run β€” hence the `rm -f` after every + # prove. A heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64`; + # a PR branch predating that fix fails at write time rather than mismeasuring. REAL_BLOCK_EPOCH_LOG2: "21" BENCH_RUNS_REAL: 1 BENCH_RUNS_PR: 3 @@ -182,8 +182,8 @@ jobs: # Real-block benchmark: same gating as growth (/bench-real, push to main, # workflow_dispatch), and for the same reason β€” it is far too slow for the - # per-comment tier. One continuation prove of the real block is ~13 min of - # CPU on this runner (147.5M cycles at the 5.31-5.62 s/Mcycle measured on + # per-comment tier. One continuation prove of the real block is ~6 min of + # CPU on this runner (~65.6M cycles at the 5.31-5.62 s/Mcycle measured on # this box in run 30631871174), against ~25s for the synthetic block, and # the runner is single and serialized across every /bench, /bench-abba and # /bench-verify in the repo. @@ -992,7 +992,7 @@ jobs: const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Synthetic screen β€” 20 plain transfers, ~16x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; + body += `Synthetic screen β€” 20 plain transfers, ~7x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Ξ” |\n`; body += `|--------|------|----|---|\n`; body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; @@ -1118,7 +1118,7 @@ jobs: // --- Footer --- if (!realTime) { body += `\n> 🧱 **This ran on synthetic blocks only.** Comment \`/bench-real\` to prove a real Ethereum block `; - body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~13 min to the run.\n`; + body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~6 min to the run.\n`; } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; diff --git a/Makefile b/Makefile index 24e930ae5..4a75f8a72 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-mat test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \ -print-real-block-fixture print-real-block-fixture-url \ +ethrex-real-block-cache print-real-block-fixture print-real-block-fixture-url \ test-ethrex-real-block-converter regen-real-block-fixture UNAME := $(shell uname) @@ -273,51 +273,108 @@ test-asm: compile-programs-asm test-rust: compile-programs-rust cargo test -p executor --test rust -# ===== Real-block benchmark fixture ===== +# ===== Real block: the benchmark workload ===== # -# A genuine Ethereum block (currently Hoodi 1265656: 4.4M gas, 11 txs, ~124 KB of contract -# bytecode, 1705 state-trie nodes), as opposed to the synthetic N-plain-transfer -# blocks from tooling/ethrex-fixtures. ~1 MB, gitignored. +# A genuine Ethereum block, as opposed to the synthetic N-plain-transfer blocks +# from tooling/ethrex-fixtures. Two artifacts, both gitignored and both FETCHED +# rather than built: # -# FETCHED, not built. Downloading the finished .bin and checking its sha256 is the -# same contract as prepare-sysroot above, and it takes the converter, the ~335-package -# ethrex host dependency tree, the ethrex-replay cache and the `rev` pin off the path -# of everyone who just wants to run a benchmark. It also decouples the block from -# what upstream happens to host: ethrex-replay publishes a cache for Hoodi and -# nothing else, so a mainnet block is unreachable by the convert-locally route but -# trivial by this one. +# the fixture the rkyv ProgramInput the benchmarks prove (~1 MB) +# the cache the ethrex-replay JSON it was converted from (~2 MB), read only +# by the converter's tests and by `regen-real-block-fixture` # -# The converter still exists and is still tested β€” see "Real-block converter" below. -# It is now a regeneration tool (ethrex rev bumps, roughly twice a year), not a -# build step. +# Fetching a verified binary is the same contract as prepare-sysroot above, and it +# keeps the converter, the ~335-package ethrex host dependency tree and an +# ethrex-replay `rev` pin off the path of everyone who just wants to run a +# benchmark. It also decouples the block from what upstream happens to host: +# ethrex-replay publishes a cache for Hoodi and nothing else, so any mainnet block +# is unreachable by the convert-locally route (its cache takes ~4 minutes and ~700 +# calls against an archive RPC to produce) and trivial by this one. # -# Which block the benchmarks run is these FOUR lines and nothing else. Every path -# below is derived from them, and the benchmark scripts / CI resolve the fixture -# through `make -s print-real-block-fixture` rather than spelling it out. No -# workflow, script or test outside this block names a block number. +# The converter still exists and is still tested β€” see "Real-block converter" +# below. It is a regeneration tool for ethrex rev bumps, not a build step. # -# Repointing: set NETWORK and the block number, upload that .bin somewhere and put -# its location and sha256 in the two lines below, then update REAL_BLOCK_FIXTURE in -# tooling/ethrex-tests so the usability screen follows. Nothing else moves β€” in -# particular the converter's own pins stay on Hoodi, because that is the one -# ethrex-replay cache upstream hosts. Full procedure and the measured cost of each -# candidate block: tooling/ethrex-real-block/README.md. -# -# Hoodi 1265656 is the current default because it is the block whose fixture and -# digest already exist, not because it is the best candidate; the mainnet blocks -# under evaluation are cheaper to prove. -ETHREX_REAL_BLOCK_NETWORK := hoodi -ETHREX_REAL_BLOCK := 1265656 -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142 -# TBD β€” the artifact is not hosted yet. Fill in with the upload location (either -# lambda.alignedlayer.com alongside the sysroot, or a GitHub Release asset). -# Deliberately empty rather than a guessed URL: an unset value fails with an -# actionable message, a wrong one fails with a 404 nobody can act on. +# ---- Repointing to a different block ---- +# These SIX lines and nothing else. Every path below derives from them, the +# benchmark scripts and CI resolve the fixture through +# `make -s print-real-block-fixture`, and no workflow, script or env var anywhere +# names a block. Outside this file the repoint touches only the converter's test +# constants and REAL_BLOCK_FIXTURE in tooling/ethrex-tests β€” both listed in +# tooling/ethrex-real-block/README.md, which also carries each candidate's +# measured cost. +ETHREX_REAL_BLOCK_NETWORK := mainnet +ETHREX_REAL_BLOCK := 25368371 +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 61eba49b6b254f4a05def5a47b08a21ae3eee56f0d37bcd7b3a24b0cc1e4a300 +ETHREX_REAL_BLOCK_CACHE_SHA256 := 7aa88a5f7c5755b7575870f95e6c5c26186947f5e9e0d52199148c74e2a2736b +# TBD β€” neither artifact is hosted yet. Fill in with the upload locations (either +# lambda.alignedlayer.com alongside the sysroot, or GitHub Release assets). +# Deliberately empty rather than guessed: an unset value fails with an actionable +# message, a wrong one fails with a 404 nobody can act on. ETHREX_REAL_BLOCK_FIXTURE_URL := +ETHREX_REAL_BLOCK_CACHE_URL := + ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin +ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_REAL_BLOCK_ID).json -ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) +# $(call ensure_verified,url,sha256,dest,label,url-var-name) +# +# Guard-then-fetch, the same shape as prepare-sysroot above: the digest of whatever +# is already on disk is checked on EVERY invocation, so this catches the three ways +# a wrong artifact gets there β€” a stale copy from before a re-upload under the same +# block number, a corrupted file, and a hand-placed one β€” not just an absent file. +# That is why these are phony targets rather than file rules: a file rule does not +# run when its target exists, which is exactly the case that needs checking. +# (It replaces the ethrex-replay rev stamp, which guarded the same class of +# staleness for the one input that used to be pinned by rev.) +# +# On a miss it downloads to a temp file and verifies BEFORE moving into place, so an +# interrupted or corrupted transfer cannot leave a file that later reads as valid and +# silently changes what every benchmark measures. The "neither sha256sum nor shasum" +# case is a hard error, as in prepare-sysroot β€” a skipped check would defeat the +# point of fetching a binary at all. +define ensure_verified + @set -e; \ + dest="$(3)"; want="$(2)"; \ + if command -v sha256sum >/dev/null 2>&1; then shacmd="sha256sum"; \ + elif command -v shasum >/dev/null 2>&1; then shacmd="shasum -a 256"; \ + else echo "$(4): missing sha256sum or shasum for checksum verification" >&2; exit 1; fi; \ + sha_of() { $$shacmd "$$1" | awk '{print $$1}'; }; \ + if [ -f "$$dest" ] && [ "$$(sha_of "$$dest")" = "$$want" ]; then \ + exit 0; \ + fi; \ + if [ -f "$$dest" ]; then \ + echo "$(4) $$dest does not match $$want - refetching."; \ + fi; \ + if [ -z "$(1)" ]; then \ + echo "$(4): $(5) is unset." >&2; \ + echo " The $(ETHREX_REAL_BLOCK_ID) $(4) is fetched, not built. Set $(5) in the" >&2; \ + echo " Makefile to wherever the artifact is hosted; see" >&2; \ + echo " tooling/ethrex-real-block/README.md for how to produce and host one." >&2; \ + exit 1; \ + fi; \ + mkdir -p $(dir $(3)); \ + tmp="$$dest.tmp"; \ + cleanup() { rm -f "$$tmp"; }; \ + trap 'cleanup' EXIT; \ + trap 'cleanup; exit 130' INT; \ + trap 'cleanup; exit 143' TERM; \ + echo "Downloading $(4) $(ETHREX_REAL_BLOCK_ID)..."; \ + curl -fL --proto '=https' --retry 3 --retry-delay 2 --retry-all-errors "$(1)" -o "$$tmp"; \ + echo "Verifying $(4) checksum..."; \ + if [ "$$(sha_of "$$tmp")" != "$$want" ]; then \ + echo "$(4): checksum mismatch for $(1)" >&2; \ + exit 1; \ + fi; \ + mv "$$tmp" "$$dest"; \ + trap - EXIT +endef + +ethrex-real-block-fixture: + $(call ensure_verified,$(ETHREX_REAL_BLOCK_FIXTURE_URL),$(ETHREX_REAL_BLOCK_FIXTURE_SHA256),$(ETHREX_REAL_BLOCK_FIXTURE),fixture,ETHREX_REAL_BLOCK_FIXTURE_URL) + +ethrex-real-block-cache: + $(call ensure_verified,$(ETHREX_REAL_BLOCK_CACHE_URL),$(ETHREX_REAL_BLOCK_CACHE_SHA256),$(ETHREX_REAL_BLOCK_CACHE),cache,ETHREX_REAL_BLOCK_CACHE_URL) # Single source of truth for the benchmark tooling. scripts/bench_verify.sh, # scripts/perf_diff.sh and .github/workflows/benchmark-pr.yml read the fixture @@ -331,91 +388,28 @@ print-real-block-fixture: print-real-block-fixture-url: @echo $(ETHREX_REAL_BLOCK_FIXTURE_URL) -# Download to a temp file and verify BEFORE moving into place, so an interrupted or -# corrupted transfer cannot leave a file that later reads as a valid fixture and -# silently changes what every benchmark measures. Checksum logic mirrors -# prepare-sysroot, including the "neither sha256sum nor shasum" hard error β€” a -# skipped check would defeat the point of fetching a binary. -$(ETHREX_REAL_BLOCK_FIXTURE): - @set -e; \ - if [ -z "$(ETHREX_REAL_BLOCK_FIXTURE_URL)" ]; then \ - echo "ethrex-real-block-fixture: ETHREX_REAL_BLOCK_FIXTURE_URL is unset." >&2; \ - echo " The $(ETHREX_REAL_BLOCK_ID) fixture is fetched, not built. Set the URL in the" >&2; \ - echo " Makefile to wherever the .bin is hosted, or regenerate locally with:" >&2; \ - echo " make regen-real-block-fixture" >&2; \ - exit 1; \ - fi; \ - mkdir -p $(dir $@); \ - tmp="$@.tmp"; \ - cleanup() { rm -f "$$tmp"; }; \ - trap 'cleanup' EXIT; \ - trap 'cleanup; exit 130' INT; \ - trap 'cleanup; exit 143' TERM; \ - echo "Downloading real-block fixture $(ETHREX_REAL_BLOCK_ID)..."; \ - curl -fL --proto '=https' --retry 3 --retry-delay 2 --retry-all-errors \ - "$(ETHREX_REAL_BLOCK_FIXTURE_URL)" -o "$$tmp"; \ - echo "Verifying fixture checksum..."; \ - checksum_ok=false; \ - if command -v sha256sum >/dev/null 2>&1; then \ - printf '%s %s\n' "$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)" "$$tmp" | sha256sum -c - >/dev/null && checksum_ok=true; \ - elif command -v shasum >/dev/null 2>&1; then \ - actual="$$(shasum -a 256 "$$tmp" | awk '{print $$1}')"; \ - [ "$$actual" = "$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)" ] && checksum_ok=true; \ - else \ - echo "ethrex-real-block-fixture: missing sha256sum or shasum for checksum verification" >&2; \ - exit 1; \ - fi; \ - if [ "$$checksum_ok" != true ]; then \ - echo "ethrex-real-block-fixture: checksum mismatch for $(ETHREX_REAL_BLOCK_FIXTURE_URL)" >&2; \ - exit 1; \ - fi; \ - mv "$$tmp" "$@"; \ - trap - EXIT - # ===== Real-block converter (regeneration tool, off the build path) ===== # -# Only needed when the guest's ethrex `rev` moves and the fixture has to be rebuilt, -# or when validating a candidate block. Nothing in the benchmark or test path builds -# this crate any more. +# Only needed when the guest's ethrex `rev` moves and the fixture has to be +# rebuilt, or when validating a candidate block. Nothing in the benchmark or test +# path builds this crate. # -# The cache stays pinned by immutable `rev`, as the guest pins ethrex itself: a -# branch ref would let the converter's reproducibility digest drift under a fixed -# input. These pins are Hoodi-only on purpose β€” that is the one cache ethrex-replay -# hosts, and it is what `conversion_is_reproducible` reads. They do NOT move when -# the benchmark block above is repointed; the two are now independent. -ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 -ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656 -ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json -# The cache filename is keyed on the block only, and its download rule has no other -# prerequisite, so make would treat an already-present cache as up to date across an -# `ETHREX_REPLAY_REV` bump and silently keep reading the old input. Depending on a -# rev-stamped marker makes a re-pin discard the stale cache; without it the mismatch -# only surfaces downstream as a `conversion_is_reproducible` digest failure, which -# reads as "regenerate the fixture" and points at the wrong thing. -ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) - -$(ETHREX_REPLAY_REV_STAMP): - mkdir -p $(dir $@) - rm -f $(ETHREX_REAL_BLOCK_CACHE) $(dir $@).replay-rev-* - touch $@ - -$(ETHREX_REAL_BLOCK_CACHE): $(ETHREX_REPLAY_REV_STAMP) - mkdir -p $(dir $@) - curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ - https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json - mv $@.tmp $@ +# The cache is fetched and verified exactly like the fixture (see ensure_verified). +# It used to come from raw.githubusercontent.com at a pinned ethrex-replay `rev`; +# that route only ever worked for the one block upstream publishes, so hosting our +# own cache is what lets the converter's tests assert against the SAME block the +# benchmarks prove rather than a second, unrelated one. # Converter correctness: host-side parity through the guest's own Crypto impl, the # network-rejection guard, and the reproducibility digest. Runs on changes to the # converter (see .github/workflows/ethrex-real-block.yml), not on every PR. -test-ethrex-real-block-converter: $(ETHREX_REAL_BLOCK_CACHE) +test-ethrex-real-block-converter: ethrex-real-block-cache cd tooling/ethrex-real-block && cargo test --release # Manual regeneration. Overwrites the fetched fixture in place so you can hash the # result and upload it β€” that upload, plus SHA256/URL above, is how the fixture is -# actually replaced. Only rebuilds the Hoodi block: a different block needs its own -# ethrex-replay cache, which upstream does not host (see the crate README). -regen-real-block-fixture: $(ETHREX_REAL_BLOCK_CACHE) +# actually replaced. +regen-real-block-fixture: ethrex-real-block-cache cd tooling/ethrex-real-block && \ cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) @@ -423,7 +417,7 @@ regen-real-block-fixture: $(ETHREX_REAL_BLOCK_CACHE) # workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock). # Needs the real-block fixture, so it needs the fixture URL to be set; CI runs the # `-offline` variant below in the PR gate and this one only in ethrex-real-block.yml. -test-ethrex: compile-programs-rust $(ETHREX_REAL_BLOCK_FIXTURE) +test-ethrex: compile-programs-rust ethrex-real-block-fixture cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm # Offline variant: no network, and what the PR gate runs. `--skip diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index a151593d9..227f049ca 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -19,17 +19,17 @@ # in the proof β€” table mix, trace lengths, query count β€” so it is the block that # decides what is being measured, not the loop around it. The synthetic default is 20 # plain transfers: ecrecover-heavy, near-empty state (9.06M cycles, 411 keccak calls, -# 80 ecsm calls). A real block inverts that mix (147.5M cycles, 9,046 keccak, 44 +# 80 ecsm calls). A real block inverts that mix (~65.6M cycles, 10,478 keccak, 116 # ecsm), so a verifier change can move the two differently. Both counts are for a # current guest ELF; they shift ~14% with ELF vintage, so pin the ELF when quoting one. # # WORKLOAD=real is therefore the honest number and WORKLOAD=synthetic the cheap one; # the default is cheap because the real block must be proven with continuations -# (~13 min per side, once, then cached in $WORK) before any verify run happens. +# (~6 min per side, once, then cached in $WORK) before any verify run happens. # Both sides always use the same block, so a comparison is never mixed. # -# Disk: a real-block continuation bundle is ~5 GB and this script CACHES both sides' -# proofs in $WORK, so budget ~10 GB there. The fixture itself is fetched by URL + +# Disk: a real-block continuation bundle is ~2 GB and this script CACHES both sides' +# proofs in $WORK, so budget ~4 GB there (more for a heavier block). The fixture itself is fetched by URL + # sha256 (see the Makefile); while that URL is unset, WORKLOAD=real cannot run. # # `/bench-verify` runs the synthetic default: that job already budgets 90 minutes diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index da03694a7..ba9ca3619 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -18,13 +18,13 @@ # # Pick the workload that matches the run you are localizing, because the symbol # mix follows the block: the synthetic default is 20 plain transfers (9.06M cycles, -# 411 keccak calls, 80 ecsm calls) and a real block inverts that (147.5M cycles, -# 9,046 keccak, 44 ecsm), so a hot symbol in one need not be hot in the other. +# 411 keccak calls, 80 ecsm calls) and a real block inverts that (~65.6M cycles, +# 10,478 keccak, 116 ecsm), so a hot symbol in one need not be hot in the other. # Both counts are for a current guest ELF; they shift ~14% with ELF vintage. # # WORKLOAD=real also switches to a continuation prove (monolithic would need -# ~700 GB at that trace length), which is ~13 min per recording β€” five recordings, -# so budget over an hour, plus ~5 GB of disk per bundle. +# ~330 GB at that trace length), which is ~6 min per recording β€” five recordings, +# so budget ~30 min, plus ~2 GB of disk per bundle. # # Produces: # - two perf-diff tables (recorded twice per side, interleaved B A B A β€” diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index edc78d2c6..b4da3e2cd 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -9,26 +9,33 @@ they execute no contract code, touch a genesis state trie only a couple of levels deep, and carry no bytecode in the witness. This tool produces the opposite β€” a block that actually looks like Ethereum. -Figures below are for the **current default** real block; a repoint replaces them -(see [Adopting a different block](#adopting-a-different-block)). +Figures below are for the **current default** real block, mainnet 25368371; a +repoint replaces them (see [Adopting a different block](#adopting-a-different-block)). +All are measured. -| | `ethrex_bench_20.bin` (synthetic) | `ethrex_hoodi_1265656.bin` (real, default) | +| | `ethrex_bench_20.bin` (synthetic) | `ethrex_mainnet_25368371.bin` (real, default) | |---|---|---| -| gas used | 420,000 | **4,402,947** | -| transactions | 20 (all plain transfers) | 11 (7Γ— EIP-1559, 4Γ— EIP-4844 blob) | -| contract calls | 0 | 5, at ~830–955k gas each | -| contract bytecode in witness | 0 | 22 contracts, ~124 KB | -| state trie | 40 accounts, fresh genesis | 1,705 nodes | -| storage keys | 0 | 422 | -| serialized size | 32,766 B | 1,021,207 B | -| cycles | 9,063,727 | **147,482,439** | -| keccak / ecsm calls | 411 / 80 | **9,046** / 44 | - -Note it has *fewer* transactions than the synthetic fixture while being ~16x the -work β€” transaction count is not the axis that matters. The crypto mix inverts too: -the synthetic block runs ~5 keccaks per ecrecover, the real one ~206. Cycle counts -are for a current guest ELF and move ~14% with ELF vintage; pin the ELF when quoting -one. +| gas used | 420,000 | **2,428,684** | +| transactions | 20 (all plain transfers) | 29 (real mix) | +| serialized size | 32,766 B | 1,110,156 B | +| cycles | 9,063,727 | **~65.6M** | +| keccak / ecsm calls | 411 / 80 | **10,478** / 116 | +| keccaks per ecrecover | 5.1 | **90** | + +Note the real block uses only ~5.8x the gas while costing ~7.2x the cycles, and +that it inverts the crypto mix: the synthetic block is ecrecover-bound, the real one +keccak- and trie-bound. That inversion is the entire point β€” a prover change can +move the two numbers in opposite directions. + +Cycle counts are for a **current guest ELF** and move ~14% with ELF vintage (this +block reads 74,819,518 on a mid-July ELF); pin the ELF whenever you quote one. + +Why this block specifically: it was the **only** block in a 90-day Dune sweep that +matched the shape constraints in the 1.6–2.6M gas band β€” exactly 2 heavy +transactions, no single whale transaction dominating, and a sane plain-transfer +share. Its composition is 11.94 tx/Mgas, 44.3% of gas in heavy transactions, 22.5% +in the top transaction, p50 transaction gas 41,297. A block that is merely *large* +is easy to find; one that is structurally typical is not. The synthetic column is `ethrex_bench_20.bin` as the benchmark scripts actually generate it β€” `ethrex-fixtures 20 … distinct`, i.e. 20 distinct genesis-funded @@ -36,7 +43,7 @@ senders to 20 distinct recipients (`scripts/bench_verify.sh`, `scripts/bench_recursion_scaling.sh`). The same block in `same` mode (one sender, one recipient) serializes to 16,811 B. -Block 1265656 is **verified to run on the guest's precompile surface** (see +Block 25368371 is **verified to run on the guest's precompile surface** (see [Validation](#validation)); it needs no accelerator we don't have. Any replacement block must clear the same check β€” that is what makes it usable, not just realistic. @@ -55,17 +62,28 @@ the same contract as `prepare-sysroot`. The file is gitignored (~1 MB; see `executor/.gitignore`), and a corrupt or interrupted download is discarded rather than left looking valid. -> The URL is currently **unset**. Until the artifact is hosted, this target fails -> with an actionable message and every consumer degrades gracefully: the benchmark -> workflow warns and skips its real-block section, and the usability screen in -> `.github/workflows/ethrex-real-block.yml` skips too. - -This crate is *not* on that path. Fetching a verified binary is what takes the -converter, the ~335-package ethrex host dependency tree, the ethrex-replay cache -and the `rev` pin off the critical path of everyone who just wants to run a -benchmark. It also decouples the block from what upstream hosts: ethrex-replay -publishes a cache for Hoodi and nothing else, so a mainnet block is unreachable by -the convert-locally route and trivial by this one. +The digest of whatever is already on disk is re-checked on every invocation, not +only when the file is missing β€” so a stale copy left over from a re-upload under the +same block number, a corrupted file, or a hand-placed one is all caught and +re-fetched. That check is the reason these are phony targets rather than file rules. + +> Both URLs are currently **unset**. Until the artifacts are hosted, these targets +> fail with an actionable message and every consumer degrades gracefully: the +> benchmark workflow warns and skips its real-block section, and the usability +> screen in `.github/workflows/ethrex-real-block.yml` skips too. + +The second artifact is the **cache** β€” the ethrex-replay JSON the fixture was +converted from (`make ethrex-real-block-cache`, ~2 MB, same verify-then-move +contract). Only the converter's tests and `regen-real-block-fixture` read it. + +This crate is *not* on the fixture's path. Fetching a verified binary takes the +converter, the ~335-package ethrex host dependency tree and an ethrex-replay `rev` +pin off the critical path of everyone who just wants to run a benchmark. It also +decouples the block from what upstream hosts: ethrex-replay publishes a cache for +Hoodi and nothing else, so any mainnet block is unreachable by the convert-locally +route β€” producing its cache takes ~4 minutes and ~700 calls against an archive RPC β€” +and trivial by this one. Hosting our own cache is what lets the converter's tests +assert against the *same* block the benchmarks prove. ## Regenerating it (ethrex rev bumps) @@ -73,14 +91,11 @@ Needed roughly twice a year, when the guest's ethrex `rev` moves and the rkyv layout changes with it: ```bash -make regen-real-block-fixture # downloads the pinned cache, rebuilds the .bin -sha256sum executor/tests/ethrex_hoodi_1265656.bin +make regen-real-block-fixture # fetches the cache, rebuilds the .bin in place +sha256sum "$(make -s print-real-block-fixture)" ``` -Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_URL` / -`_SHA256`. The cache URL is pinned to an ethrex-replay **commit**, not `main` -(`ETHREX_REPLAY_REV`), matching how the guest pins ethrex β€” a branch ref would let -the derivation drift under a fixed input. +Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_SHA256` and its URL. Directly, against any cache file: @@ -92,26 +107,26 @@ cargo run --release -- Output is deterministic for a given cache file: ```text -ethrex_hoodi_1265656.bin - block: hoodi #1265656 β€” 11 transactions, 4,402,947 gas - sha256: 1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142 - source: ethrex-replay caches/cache_hoodi_1265656.json @ 2693e018 +wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110156 bytes): 1 block(s) \ + from mainnet starting at #25368371, 29 transaction(s), 2428684 gas ``` -That checksum is enforced, not just documented: `conversion_is_reproducible` -asserts it. The pinned commit keeps the *input* immutable; the digest keeps the -*derivation* honest. +That determinism is enforced, not just documented: `conversion_is_reproducible` +pins the block's stats **and** the fixture's sha256, so the digest keeps the +derivation honest. Verified: regenerating from the cache reproduces +`61eba49b…` byte for byte, which is also what proves the hosted `.bin` and the +hosted cache describe the same block. ## What benchmarks with it -Costs below are for the **current default block** (hoodi 1265656) and move with it β€” -see [Measured cost of candidate blocks](#measured-cost-of-candidate-blocks). +Costs below are for the **current default block** (mainnet 25368371) and move with +it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-blocks). | Where | How to run it | Cost (current default) | |---|---|---| -| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~13 min | -| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~13 min per side, then cached | -| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so >1 h | +| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~6 min | +| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~6 min per side, then cached | +| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~30 min | None of them hardcode the fixture path or a block number β€” they read the path from `make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when @@ -120,21 +135,22 @@ the `.bin` is absent. **Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic prove grows ~4.9 GB per million cycles on this workload family (measured on the bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), -so a real block in the 110–150M cycle range needs **500–700 GB**. `--continuations` -makes peak heap a function of the epoch size instead of the trace length, so the -same block fits in **~19–21 GB** at epoch 2^21: a 15.8 GB epoch working set plus -~69 MB per epoch of accumulated proofs. The bundle on disk is ~4–5 GB, and -serializing it past 2 GiB needs rkyv `pointer_width_64`. +so this block would need **~330 GB** monolithically β€” and a heavier candidate up to +~700 GB. `--continuations` makes peak heap a function of the epoch size instead of +the trace length, so the same block fits in **~18 GB** at epoch 2^21: a 15.8 GB +epoch working set plus ~60 MB per epoch of accumulated proofs over ~32 epochs. The +bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which +needs rkyv `pointer_width_64` to serialize. Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in -~25s against ~13 min, and one runner carries every `/bench`, `/bench-abba` and +~25s against ~6 min, and one runner carries every `/bench`, `/bench-abba` and `/bench-verify` in the repo. The synthetic number is a fast screen; the real block is the number that means something. -Cycle counts here (9.06M synthetic, 147.5M real) are for a **current guest ELF**. -They move ~14% with ELF vintage β€” the same Hoodi block reads 168.3M on a -mid-July ELF β€” so pin the ELF whenever you quote one, or it will look like a -regression the next time someone measures. +Cycle counts here (9.06M synthetic, ~65.6M real) are for a **current guest ELF**. +They move ~14% with ELF vintage β€” this block reads 74,819,518 on a mid-July ELF β€” so +pin the ELF whenever you quote one, or it will look like a regression the next time +someone measures. ## Where validation runs @@ -181,28 +197,36 @@ refuses instead; `unmappable_network_is_rejected` pins that. ## Adopting a different block -Since the fixture is fetched rather than converted locally, the **benchmark block -and this crate's test block are independent**. This crate stays pinned to Hoodi -1265656 β€” that is the one cache ethrex-replay hosts, and `conversion_is_reproducible` -reads it β€” and none of its pins move when the benchmark block changes. +Because we host both artifacts, the converter's tests assert against the **same** +block the benchmarks prove, so a repoint moves them together. Two files: -Swapping the benchmark block is **four lines**, once the new `.bin` is hosted: +**1. The Makefile β€” the only place a block number appears.** Six lines: ```make -# Makefile β€” the ONLY place a block number appears ETHREX_REAL_BLOCK_NETWORK := ETHREX_REAL_BLOCK := ETHREX_REAL_BLOCK_FIXTURE_SHA256 := -ETHREX_REAL_BLOCK_FIXTURE_URL := +ETHREX_REAL_BLOCK_CACHE_SHA256 := +ETHREX_REAL_BLOCK_FIXTURE_URL := +ETHREX_REAL_BLOCK_CACHE_URL := ``` -plus `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests` (which points the usability -screen at the block actually being benchmarked). Everything else derives from the -Makefile β€” the fixture name, and through `make -s print-real-block-fixture` the -benchmark scripts and `benchmark-pr.yml`. No workflow, script or env var names a -block. Nothing in `executor/.gitignore` needs touching either: it already ignores -every accepted network's fixture name, so a repointed ~1 MB fixture cannot become -committable by accident. +**2. The test constants**, which are what prove the repoint is self-consistent: +- `src/main.rs` β€” `CACHE`, `CACHE_MISSING`, the chain-ID import and assert + (`MAINNET_CHAIN_ID` / `HOODI_CHAIN_ID` / `SEPOLIA_CHAIN_ID` from + `ethrex_config::networks`), and in `conversion_is_reproducible` the + `first_block_number` / `transactions` / `gas_used` / digest asserts. +- `tooling/ethrex-tests` β€” `REAL_BLOCK_FIXTURE`, which points the usability screen + at the block actually being benchmarked. + +Then run `make test-ethrex-real-block-converter`. It passing means the cache, the +`.bin`, the digest and the block stats all describe one block. + +Everything else derives from the Makefile β€” the fixture name, and through +`make -s print-real-block-fixture` the benchmark scripts and `benchmark-pr.yml`. No +workflow, script or env var names a block. Nothing in `executor/.gitignore` needs +touching either: it already ignores every accepted network's fixture name, so a +repointed ~1 MB fixture cannot become committable by accident. ### Measured cost of candidate blocks @@ -210,34 +234,47 @@ Cost is a property of the block, so it changes with the repoint. Cycles are give for a **current guest ELF**; prove time and heap are for the CPU bench runner at epoch 2^21, using the 5.31–5.62 s per Mcycle measured on that box. -| block | cycles | prove | peak heap | fixture | -|---|---|---|---|---| -| hoodi 1265656 β€” *current default* | 147.5M | ~13 min | ~21 GB | 1,021,207 B, `1f7d4c4c…` | -| mainnet 25453112 | ~110M | ~10 min | ~19 GB | 2,019,747 B, `0298663d…` | +| block | gas | cycles | prove | peak heap | proof | fixture | +|---|---|---|---|---|---|---| +| **mainnet 25368371** β€” *current default* | 2.43M | **~65.6M** | **~6 min** | ~18 GB | ~1.9 GB | 1,110,156 B, `61eba49b…` | +| mainnet 25453112 | 4.24M | ~110M | ~10 min | ~19 GB | ~3.7 GB | 2,019,747 B, `0298663d…` | +| hoodi 1265656 | 4.40M | ~147.5M | ~13 min | ~21 GB | ~4.7 GB | 1,021,207 B, `1f7d4c4c…` | -These are measurements, not estimates: the mainnet block executes in 125,932,956 -cycles on the mid-July ELF against Hoodi's 168,319,360 on the same ELF, i.e. ~25% -**cheaper** despite being the larger fixture β€” cycles per gas is not constant across -blocks. Both clear the usability screen. Further candidates are being measured; add -a row rather than editing the wiring. +These are measurements, not estimates. All three clear the usability screen. Add a +row rather than editing the wiring. -Hoodi is the default only because its fixture and digest already exist. Note it is -also the only block `make regen-real-block-fixture` can rebuild: that route needs an -ethrex-replay cache, upstream hosts Hoodi's alone, and producing another takes ~4 -minutes and ~700 calls against an archive RPC. That asymmetry is exactly why the -fixture is fetched rather than converted. +Two things the table shows that a gas-based estimate would have got wrong. **Cycles +per gas is not constant** β€” it ranges ~26–38 across these blocks β€” so ranking +candidates by gas mispredicts cost; 25453112 has *more* gas than hoodi 1265656 yet +costs ~25% fewer cycles. And **fixture size does not track cost** either: the current +default is the cheapest block and the middle-sized fixture. -Benchmark comparability does not survive the swap, and that is intentional rather -than a wrinkle to work around: `benchmark-pr.yml` records which block it measured -and refuses to diff a PR against a baseline that measured a different one, so the -first run after a repoint reports one-sided numbers until main republishes. +The default is the cheapest of the three, which matters because this workload sits on +a single shared bench runner. It was also the only block in a 90-day Dune sweep +matching the shape constraints (2 heavy transactions, no whale, sane transfer share) +in the 1.6–2.6M gas band β€” so it is cheap *and* structurally typical, not cheap +because it is degenerate. + +### Verifying a repoint + +Run **both**, in this order: -Then run **`make test-ethrex`**, not just this crate's tests. A new block is only +```bash +make test-ethrex-real-block-converter # cache, .bin, digest and stats agree +make test-ethrex # the block is USABLE on the guest +``` + +The second is not optional and the first cannot replace it: a new block is only usable if it needs no accelerator the guest lacks, and this crate cannot tell you that β€” its graph links a working c-kzg, so a block calling point evaluation (0x0a) passes here and fails in the guest. `test_ethrex_real_block_native` in `tooling/ethrex-tests` is the screen. See [Validation](#validation). +Benchmark comparability does not survive the swap, and that is intentional rather +than a wrinkle to work around: `benchmark-pr.yml` records which block it measured +and refuses to diff a PR against a baseline that measured a different one, so the +first run after a repoint reports one-sided numbers until main republishes. + ## Why the JSON and not ethrex-replay's own `.bin` `ethrex-replay` can already emit a rkyv `ProgramInput`, but it tracks ethrex diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index f63ef97c5..818ecbdac 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -113,15 +113,15 @@ fn main() -> Result<(), Box> { #[cfg(test)] mod tests { use super::*; - use ethrex_config::networks::HOODI_CHAIN_ID; + use ethrex_config::networks::MAINNET_CHAIN_ID; - const CACHE: &str = "caches/cache_hoodi_1265656.json"; + const CACHE: &str = "caches/cache_mainnet_25368371.json"; /// `caches/` is gitignored and fetched on demand, so every test here fails on /// a clean checkout until the cache is downloaded. Say so instead of surfacing /// a bare `No such file or directory` from `unwrap()`. - const CACHE_MISSING: &str = "caches/cache_hoodi_1265656.json is missing β€” run \ - `make ethrex-real-block-fixture` from the repo root first"; + const CACHE_MISSING: &str = "caches/cache_mainnet_25368371.json is missing β€” run \ + `make ethrex-real-block-cache` from the repo root first"; /// Executes the converted block with `LambdaVmEcsmCrypto`, the `Crypto` impl /// the guest injects, so the block is exercised through the same trait @@ -186,9 +186,9 @@ mod tests { let (program_input, summary) = program_input_from_cache(CACHE).expect(CACHE_MISSING); let bytes = rkyv::to_bytes::(&program_input).unwrap(); - assert_eq!(summary.first_block_number, 1_265_656); - assert_eq!(summary.transactions, 11); - assert_eq!(summary.gas_used, 4_402_947); + assert_eq!(summary.first_block_number, 25_368_371); + assert_eq!(summary.transactions, 29); + assert_eq!(summary.gas_used, 2_428_684); // Asserted separately from the digest below, not covered by it. The digest // is exactly what a legitimate ethrex rev bump forces someone to rewrite @@ -196,8 +196,8 @@ mod tests { // that moment it stops covering the substituted-chain-config case it was // chosen for. This assert survives that churn. assert_eq!( - program_input.execution_witness.chain_config.chain_id, HOODI_CHAIN_ID, - "chain config is not Hoodi's β€” the block would replay under other rules", + program_input.execution_witness.chain_config.chain_id, MAINNET_CHAIN_ID, + "chain config is not mainnet's β€” the block would replay under other rules", ); // Digest, not length: a layout change can preserve the byte count exactly @@ -208,7 +208,7 @@ mod tests { .map(|b| format!("{b:02x}")) .collect(); assert_eq!( - digest, "1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142", + digest, "61eba49b6b254f4a05def5a47b08a21ae3eee56f0d37bcd7b3a24b0cc1e4a300", "fixture bytes changed β€” regenerate it and update the README checksum", ); } diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 26f991c7a..250e2411f 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -610,6 +610,7 @@ version = "0.1.0" dependencies = [ "k256", "num-bigint", + "num-integer", "num-traits", ] diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index b59640a1f..d7026dca6 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -87,7 +87,7 @@ fn test_ethrex_empty_block() { run_program_and_check_public_output(ELF_PATH, output.encode(), inputs); } -const REAL_BLOCK_FIXTURE: &str = "ethrex_hoodi_1265656.bin"; +const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin"; /// Host-only acceptance gate for the real-block fixture produced by /// `tooling/ethrex-real-block` (`make ethrex-real-block-fixture`): the block is From 19ed945c13bb62838de9d7611794ec84cbe2b3bd Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:23:59 -0300 Subject: [PATCH 14/27] bench(ethrex): repoint the benchmark block to mainnet 25368371 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repoints the benchmark fixture only. The converter's test constants stay on Hoodi 1265656 and do NOT move with it: what they exercise is the CONVERSION, which any real block demonstrates equally well, and Hoodi's is the one cache ethrex-replay publishes upstream β€” so pinning there costs us no hosting and cannot drift. The fetch is what makes that decoupling possible, and it means a future repoint touches the Makefile plus one constant in tooling/ethrex-tests, never this crate. That restores ETHREX_REPLAY_REV and its rev stamp for the converter's cache, so 05e02851 is no longer superseded. The benchmark block's own cache is fetched from our release alongside the fixture and read only by regen-real-block-fixture; the two caches are now separate variables (ETHREX_CONVERTER_CACHE vs ETHREX_REAL_BLOCK_CACHE) because they serve different purposes and move independently. The fixture URLs are live (bench-fixtures-v1 release), so the TBDs are filled in. The unset-URL guards stay: they now cover the window after a repoint but before the new artifact is uploaded, which is exactly when a push to main would otherwise go red over an upload nobody in CI can perform. Block: 2,428,684 gas, 29 txs, ~65.6M cycles on a current guest ELF (74,819,518 on the mid-July one), 10,478 keccak, 116 ECSM, ~6 min per prove, ~18 GB peak, ~1.9 GB bundle. Only block in a 90-day Dune sweep matching the shape constraints in the 1.6-2.6M gas band (2 heavy txs, no whale, sane transfer mix; heavy-gas 44.3%, top-tx 22.5%, p50 tx gas 41,297), and pre-FWA so transient-clean. Also the cheapest of the three measured candidates, which matters on a shared runner. Validation, all run end to end against the live release: - make ethrex-real-block-fixture from a clean slate: fetch -> checksum -> place, digest 61eba49b... matches. Same for both caches (release + upstream). - unset-URL path still reads correctly for the default block. - converter tests 3/3 against the Hoodi pins. - test_ethrex_real_block_native and no_kzg_backend_linked pass on the mainnet block. --- .github/workflows/benchmark-pr.yml | 7 +- .github/workflows/ethrex-real-block.yml | 6 +- Makefile | 68 +++++++++++++------ tooling/ethrex-real-block/README.md | 89 ++++++++++++++----------- tooling/ethrex-real-block/src/main.rs | 26 +++++--- 5 files changed, 121 insertions(+), 75 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 61f7d3903..6d893496c 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -198,9 +198,10 @@ jobs: fi # The fixture is FETCHED from a pinned URL + sha256, not built (see the - # Makefile). While that URL is unset the block cannot be obtained at all, so - # degrade to the synthetic sections with a warning rather than failing β€” - # otherwise every push to main goes red over an upload nobody in CI can do. + # Makefile). The guard covers the window after a repoint but before the new + # artifact is uploaded: degrade to the synthetic sections with a warning + # rather than failing, so a push to main cannot go red over an upload nobody + # in CI can perform. if [ "$RUN_REAL" = "true" ] && [ -z "$(make -s print-real-block-fixture-url)" ]; then echo "::warning::Real-block benchmark skipped: ETHREX_REAL_BLOCK_FIXTURE_URL is unset in the Makefile." RUN_REAL=false diff --git a/.github/workflows/ethrex-real-block.yml b/.github/workflows/ethrex-real-block.yml index 8331d8327..460025102 100644 --- a/.github/workflows/ethrex-real-block.yml +++ b/.github/workflows/ethrex-real-block.yml @@ -84,9 +84,9 @@ jobs: tooling/ethrex-tests -> target # Fetch-and-verify, not build: no converter, no ethrex-replay cache, no rev pin. - # Skipped while the fixture has no host β€” the screen below is the only consumer, - # and failing the job on an unset URL would block PRs on an upload nobody in the - # PR can perform. Remove this guard once ETHREX_REAL_BLOCK_FIXTURE_URL is set. + # The guard covers the window after a repoint but before the new artifact is + # uploaded: the screen below is its only consumer, and failing the job on an + # unset URL would block PRs on an upload nobody in the PR can perform. - name: Fetch real-block fixture id: fixture run: | diff --git a/Makefile b/Makefile index 4a75f8a72..4b90a786f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-mat test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \ -ethrex-real-block-cache print-real-block-fixture print-real-block-fixture-url \ +ethrex-real-block-cache ethrex-real-block-converter-cache print-real-block-fixture \ +print-real-block-fixture-url \ test-ethrex-real-block-converter regen-real-block-fixture UNAME := $(shell uname) @@ -295,23 +296,22 @@ test-rust: compile-programs-rust # below. It is a regeneration tool for ethrex rev bumps, not a build step. # # ---- Repointing to a different block ---- -# These SIX lines and nothing else. Every path below derives from them, the +# These FIVE lines and nothing else. Every path below derives from them, the # benchmark scripts and CI resolve the fixture through # `make -s print-real-block-fixture`, and no workflow, script or env var anywhere -# names a block. Outside this file the repoint touches only the converter's test -# constants and REAL_BLOCK_FIXTURE in tooling/ethrex-tests β€” both listed in -# tooling/ethrex-real-block/README.md, which also carries each candidate's +# names a block. Outside this file the repoint touches only REAL_BLOCK_FIXTURE in +# tooling/ethrex-tests, which points the usability screen at the block actually +# being proven. The converter's own pins do NOT move β€” see below. +# tooling/ethrex-real-block/README.md carries the procedure and each candidate's # measured cost. ETHREX_REAL_BLOCK_NETWORK := mainnet ETHREX_REAL_BLOCK := 25368371 +ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371.bin ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 61eba49b6b254f4a05def5a47b08a21ae3eee56f0d37bcd7b3a24b0cc1e4a300 +# The block's source cache, hosted in the same release. Only `regen-real-block-fixture` +# reads it β€” the converter's TESTS use a different, upstream-pinned cache (below). +ETHREX_REAL_BLOCK_CACHE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/cache_mainnet_25368371.json ETHREX_REAL_BLOCK_CACHE_SHA256 := 7aa88a5f7c5755b7575870f95e6c5c26186947f5e9e0d52199148c74e2a2736b -# TBD β€” neither artifact is hosted yet. Fill in with the upload locations (either -# lambda.alignedlayer.com alongside the sysroot, or GitHub Release assets). -# Deliberately empty rather than guessed: an unset value fails with an actionable -# message, a wrong one fails with a 404 nobody can act on. -ETHREX_REAL_BLOCK_FIXTURE_URL := -ETHREX_REAL_BLOCK_CACHE_URL := ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin @@ -394,21 +394,49 @@ print-real-block-fixture-url: # rebuilt, or when validating a candidate block. Nothing in the benchmark or test # path builds this crate. # -# The cache is fetched and verified exactly like the fixture (see ensure_verified). -# It used to come from raw.githubusercontent.com at a pinned ethrex-replay `rev`; -# that route only ever worked for the one block upstream publishes, so hosting our -# own cache is what lets the converter's tests assert against the SAME block the -# benchmarks prove rather than a second, unrelated one. +# Its TEST input is pinned to Hoodi 1265656, independently of whichever block the +# benchmarks currently prove, and stays there across a repoint. What these tests +# exercise is the CONVERSION β€” cache JSON in, correctly-laid-out rkyv out β€” which +# any real block demonstrates equally well. Hoodi's is the one cache ethrex-replay +# publishes, so pinning there costs us no hosting, cannot drift, and leaves the +# benchmark block free to change without touching this crate. +# +# Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would let +# the converter's reproducibility digest drift under a fixed input. +ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 +ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656 +ETHREX_CONVERTER_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json +# The cache filename is keyed on the block only, and its download rule has no other +# prerequisite, so make would treat an already-present cache as up to date across an +# `ETHREX_REPLAY_REV` bump and silently keep reading the old input. Depending on a +# rev-stamped marker makes a re-pin discard the stale cache; without it the mismatch +# only surfaces downstream as a `conversion_is_reproducible` digest failure, which +# reads as "regenerate the fixture" and points at the wrong thing. +ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) + +$(ETHREX_REPLAY_REV_STAMP): + mkdir -p $(dir $@) + rm -f $(ETHREX_CONVERTER_CACHE) $(dir $@).replay-rev-* + touch $@ + +$(ETHREX_CONVERTER_CACHE): $(ETHREX_REPLAY_REV_STAMP) + mkdir -p $(dir $@) + curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \ + https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json + mv $@.tmp $@ + +ethrex-real-block-converter-cache: $(ETHREX_CONVERTER_CACHE) # Converter correctness: host-side parity through the guest's own Crypto impl, the # network-rejection guard, and the reproducibility digest. Runs on changes to the # converter (see .github/workflows/ethrex-real-block.yml), not on every PR. -test-ethrex-real-block-converter: ethrex-real-block-cache +test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE) cd tooling/ethrex-real-block && cargo test --release -# Manual regeneration. Overwrites the fetched fixture in place so you can hash the -# result and upload it β€” that upload, plus SHA256/URL above, is how the fixture is -# actually replaced. +# Manual regeneration of the BENCHMARK fixture (not the converter's test block): +# fetches that block's own cache and re-converts it, overwriting the fixture in +# place so you can hash the result and upload it. That upload, plus SHA256/URL at +# the top, is how the fixture is actually replaced. regen-real-block-fixture: ethrex-real-block-cache cd tooling/ethrex-real-block && \ cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index b4da3e2cd..640e293d5 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -67,31 +67,38 @@ only when the file is missing β€” so a stale copy left over from a re-upload und same block number, a corrupted file, or a hand-placed one is all caught and re-fetched. That check is the reason these are phony targets rather than file rules. -> Both URLs are currently **unset**. Until the artifacts are hosted, these targets -> fail with an actionable message and every consumer degrades gracefully: the -> benchmark workflow warns and skips its real-block section, and the usability -> screen in `.github/workflows/ethrex-real-block.yml` skips too. +Artifacts live in the **[`bench-fixtures-v1`][release]** release on +`yetanotherco/lambda_vm`, fetched unauthenticated: -The second artifact is the **cache** β€” the ethrex-replay JSON the fixture was -converted from (`make ethrex-real-block-cache`, ~2 MB, same verify-then-move -contract). Only the converter's tests and `regen-real-block-fixture` read it. +| asset | sha256 | read by | +|---|---|---| +| `ethrex_mainnet_25368371.bin` | `61eba49b…` | every benchmark (**current default**) | +| `cache_mainnet_25368371.json` | `7aa88a5f…` | `regen-real-block-fixture` | +| `ethrex_mainnet_25453112.bin` | `0298663d…` | alternate candidate | +| `cache_mainnet_25453112.json` | `20ffbbc1…` | alternate candidate | + +Each block has two assets: the fixture and the **cache** it was converted from +(`make ethrex-real-block-cache`, ~2 MB, same verify-then-move contract). Only +`regen-real-block-fixture` reads the cache. Note it is *not* the cache the +converter's own tests use β€” see [Validation](#validation). -This crate is *not* on the fixture's path. Fetching a verified binary takes the +This crate is not on the fixture's path at all. Fetching a verified binary takes the converter, the ~335-package ethrex host dependency tree and an ethrex-replay `rev` pin off the critical path of everyone who just wants to run a benchmark. It also -decouples the block from what upstream hosts: ethrex-replay publishes a cache for -Hoodi and nothing else, so any mainnet block is unreachable by the convert-locally -route β€” producing its cache takes ~4 minutes and ~700 calls against an archive RPC β€” -and trivial by this one. Hosting our own cache is what lets the converter's tests -assert against the *same* block the benchmarks prove. +decouples the benchmark block from what upstream hosts: ethrex-replay publishes a +cache for Hoodi and nothing else, so any mainnet block is unreachable by the +convert-locally route β€” producing its cache takes ~4 minutes and ~700 calls against +an archive RPC β€” and trivial by this one. + +[release]: https://github.com/yetanotherco/lambda_vm/releases/tag/bench-fixtures-v1 -## Regenerating it (ethrex rev bumps) +## Regenerating the fixture (ethrex rev bumps) Needed roughly twice a year, when the guest's ethrex `rev` moves and the rkyv layout changes with it: ```bash -make regen-real-block-fixture # fetches the cache, rebuilds the .bin in place +make regen-real-block-fixture # fetches that block's cache, rebuilds the .bin sha256sum "$(make -s print-real-block-fixture)" ``` @@ -111,11 +118,11 @@ wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110156 bytes): 1 block( from mainnet starting at #25368371, 29 transaction(s), 2428684 gas ``` -That determinism is enforced, not just documented: `conversion_is_reproducible` -pins the block's stats **and** the fixture's sha256, so the digest keeps the -derivation honest. Verified: regenerating from the cache reproduces -`61eba49b…` byte for byte, which is also what proves the hosted `.bin` and the -hosted cache describe the same block. +Verified: regenerating from the hosted cache reproduces `61eba49b…` byte for byte, +which is what proves the hosted `.bin` and the hosted cache describe the same block. + +The converter's `conversion_is_reproducible` test enforces the same property, but +against its own pinned block rather than this one β€” see [Validation](#validation). ## What benchmarks with it @@ -197,30 +204,27 @@ refuses instead; `unmappable_network_is_rejected` pins that. ## Adopting a different block -Because we host both artifacts, the converter's tests assert against the **same** -block the benchmarks prove, so a repoint moves them together. Two files: +The benchmark block and this crate's test block are **independent** β€” the fetch is +what decouples them β€” so a repoint touches two files and neither is this crate's +source. -**1. The Makefile β€” the only place a block number appears.** Six lines: +**1. The Makefile β€” the only place a block number appears.** Five lines: ```make ETHREX_REAL_BLOCK_NETWORK := ETHREX_REAL_BLOCK := +ETHREX_REAL_BLOCK_FIXTURE_URL := ETHREX_REAL_BLOCK_FIXTURE_SHA256 := -ETHREX_REAL_BLOCK_CACHE_SHA256 := -ETHREX_REAL_BLOCK_FIXTURE_URL := -ETHREX_REAL_BLOCK_CACHE_URL := +ETHREX_REAL_BLOCK_CACHE_URL / _SHA256 := ``` -**2. The test constants**, which are what prove the repoint is self-consistent: -- `src/main.rs` β€” `CACHE`, `CACHE_MISSING`, the chain-ID import and assert - (`MAINNET_CHAIN_ID` / `HOODI_CHAIN_ID` / `SEPOLIA_CHAIN_ID` from - `ethrex_config::networks`), and in `conversion_is_reproducible` the - `first_block_number` / `transactions` / `gas_used` / digest asserts. -- `tooling/ethrex-tests` β€” `REAL_BLOCK_FIXTURE`, which points the usability screen - at the block actually being benchmarked. +**2. `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests`**, which points the usability +screen at the block actually being benchmarked. That is the whole of it. -Then run `make test-ethrex-real-block-converter`. It passing means the cache, the -`.bin`, the digest and the block stats all describe one block. +**Nothing in this crate moves.** Its test constants stay pinned to Hoodi 1265656 +across every repoint β€” what they exercise is the conversion, not the workload, and +Hoodi's is the one cache ethrex-replay publishes, so pinning there costs no hosting +and cannot drift. Everything else derives from the Makefile β€” the fixture name, and through `make -s print-real-block-fixture` the benchmark scripts and `benchmark-pr.yml`. No @@ -260,11 +264,12 @@ because it is degenerate. Run **both**, in this order: ```bash -make test-ethrex-real-block-converter # cache, .bin, digest and stats agree +make ethrex-real-block-fixture # fetch + verify the new .bin make test-ethrex # the block is USABLE on the guest ``` -The second is not optional and the first cannot replace it: a new block is only +`make test-ethrex` is the one that matters here, and the converter's tests cannot +replace it β€” they run against a different block. A new block is only usable if it needs no accelerator the guest lacks, and this crate cannot tell you that β€” its graph links a working c-kzg, so a block calling point evaluation (0x0a) passes here and fails in the guest. `test_ethrex_real_block_native` in @@ -307,6 +312,11 @@ path-filtered workflow rather than the PR gate (see [Where validation runs](#where-validation-runs)); `ethrex-real-block.yml` caches this workspace's `target/` under its own key, so only cold runs pay it. +These run against this crate's own pinned block (Hoodi 1265656), **not** the +benchmark block β€” they test the conversion, which any real block exercises equally, +and Hoodi's is the one cache ethrex-replay publishes. Only +`test_ethrex_real_block_native` follows the benchmark block. + **`cargo test` here β€” `real_block_executes_under_guest_crypto`.** Executes the block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is exercised via the guest's own trait dispatch. Stateless re-execution ends in a @@ -345,8 +355,9 @@ instead of silently producing a fixture the guest can't read. KZG backend. That was incidental to its dependency graph, and it is the property the next check relies on, so it is pinned here rather than assumed. -**`tooling/ethrex-tests` β€” `test_ethrex_real_block_native`.** Checks the -serialized `.bin` itself deserializes and executes. Since that crate links no KZG +**`tooling/ethrex-tests` β€” `test_ethrex_real_block_native`.** The one check that +follows the BENCHMARK block. Checks the serialized `.bin` itself deserializes and +executes. Since that crate links no KZG backend, this is also **what screens point evaluation (0x0a)**: a block reaching it diverges from consensus and fails here. diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-real-block/src/main.rs index 818ecbdac..4949242a4 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-real-block/src/main.rs @@ -113,15 +113,21 @@ fn main() -> Result<(), Box> { #[cfg(test)] mod tests { use super::*; - use ethrex_config::networks::MAINNET_CHAIN_ID; + use ethrex_config::networks::HOODI_CHAIN_ID; - const CACHE: &str = "caches/cache_mainnet_25368371.json"; + const CACHE: &str = "caches/cache_hoodi_1265656.json"; + /// This crate is pinned to Hoodi 1265656 on purpose, independently of whichever + /// block the benchmarks currently prove: it is the one cache ethrex-replay hosts + /// upstream, so keeping the converter's test input there costs us no hosting and + /// cannot drift. What is under test here is the CONVERSION, not the benchmark + /// workload β€” see tooling/ethrex-real-block/README.md. + /// /// `caches/` is gitignored and fetched on demand, so every test here fails on /// a clean checkout until the cache is downloaded. Say so instead of surfacing /// a bare `No such file or directory` from `unwrap()`. - const CACHE_MISSING: &str = "caches/cache_mainnet_25368371.json is missing β€” run \ - `make ethrex-real-block-cache` from the repo root first"; + const CACHE_MISSING: &str = "caches/cache_hoodi_1265656.json is missing β€” run \ + `make ethrex-real-block-converter-cache` from the repo root first"; /// Executes the converted block with `LambdaVmEcsmCrypto`, the `Crypto` impl /// the guest injects, so the block is exercised through the same trait @@ -186,9 +192,9 @@ mod tests { let (program_input, summary) = program_input_from_cache(CACHE).expect(CACHE_MISSING); let bytes = rkyv::to_bytes::(&program_input).unwrap(); - assert_eq!(summary.first_block_number, 25_368_371); - assert_eq!(summary.transactions, 29); - assert_eq!(summary.gas_used, 2_428_684); + assert_eq!(summary.first_block_number, 1_265_656); + assert_eq!(summary.transactions, 11); + assert_eq!(summary.gas_used, 4_402_947); // Asserted separately from the digest below, not covered by it. The digest // is exactly what a legitimate ethrex rev bump forces someone to rewrite @@ -196,8 +202,8 @@ mod tests { // that moment it stops covering the substituted-chain-config case it was // chosen for. This assert survives that churn. assert_eq!( - program_input.execution_witness.chain_config.chain_id, MAINNET_CHAIN_ID, - "chain config is not mainnet's β€” the block would replay under other rules", + program_input.execution_witness.chain_config.chain_id, HOODI_CHAIN_ID, + "chain config is not Hoodi's β€” the block would replay under other rules", ); // Digest, not length: a layout change can preserve the byte count exactly @@ -208,7 +214,7 @@ mod tests { .map(|b| format!("{b:02x}")) .collect(); assert_eq!( - digest, "61eba49b6b254f4a05def5a47b08a21ae3eee56f0d37bcd7b3a24b0cc1e4a300", + digest, "1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142", "fixture bytes changed β€” regenerate it and update the README checksum", ); } From c084564478bea45758fd941ee8b51696b8503b58 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:29:23 -0300 Subject: [PATCH 15/27] bench(gpu): make the real block the default /bench-gpu workload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GPU benchmark ran the synthetic 100-transfer fixture; it now runs the real block, and unlike the CPU path the real block is the DEFAULT here rather than opt-in. The two paths differ because their scarce resource differs: /bench-gpu rents its own Vast box per run, so the cost is money and nothing queues behind it, while the CPU bench shares one runner with every /bench, /bench-abba and /bench-verify in the repo. Same reasoning, opposite conclusion. Synthetic workloads stay reachable through the existing `cont[TX]` / `mono[TX]` tokens with unchanged semantics, so every number recorded before this reproduces with the exact syntax that produced it. A new `real` token names the default explicitly. Implementation follows the pattern already used by bench_verify.sh and perf_diff.sh: scripts/bench_abba.sh gains WORKLOAD=synthetic|real (defaulting to synthetic, so CPU /bench-abba is untouched), resolves the fixture through `make -s print-real-block-fixture`, and fetches it when absent β€” which a rented box always needs, since it is a fresh checkout. That fetch is a ~1 MB URL + sha256 download, not a build, so it costs seconds of rental. WORKLOAD=real forces --continuations rather than offering it: monolithic would only OOM at this trace length. Deliberately NOT touched, per the GPU sweep's tuning: LAMBDA_VM_VRAM_BUDGET_MB, mempool settings, EPOCH_SIZE_LOG2, K, and the offer query's values. Changing any of them silently would break comparability with the sweep that chose them. No GPU duration is asserted anywhere, because none has been measured β€” the CPU rate does not transfer, and the sweep's finding that the prover is CPU-bound at the serial producer above epoch 2^21 cuts against assuming a large speedup. The "running…" comment now says the real-block runtime is unmeasured and that the run establishes the baseline, instead of quoting the synthetic workload's ~2.5 hr. Two things checked rather than assumed: - The box RAM floor stays 48 GB and needs no change. Continuation peak heap is set by the epoch size, not the block, so the real block costs the same ~10 GB working set at the default epoch 2^20 plus ~4.6 GB of accumulated epoch proofs β€” ~14 GB, or ~18 GB at 2^21. The comment now records that instead of leaving the next reader to re-derive it. - The rkyv pointer_width_64 fail-fast is now scoped to synthetic >=40tx. It exists because a large synthetic continuation bundle exceeds the old 2 GiB cap; the real block's bundle is ~1.9 GB, so applying the guard to it would reject PR branches over a limit they do not reach. This workflow never checks the repo out (it orchestrates over the Vast API and SSH), so the workload label is a fixed string rather than a make query; bench_abba.sh prints the resolved fixture path on the box and that lands in the run log. --- .github/workflows/benchmark-gpu.yml | 72 ++++++++++++++++++++--------- scripts/bench_abba.sh | 54 +++++++++++++++++++--- tooling/ethrex-real-block/README.md | 16 +++++++ 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 040b51c89..c6de983d8 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -6,10 +6,16 @@ name: Benchmark GPU (PR) # It builds the cli at the PR head and at main, runs N interleaved pairs on the GPU, # posts the paired-t + Wilcoxon verdict back to the PR, then ALWAYS destroys the box. # -# Triggered by a "/bench-gpu [N] [cont[TX]|mono[TX]]" comment on a PR (N = pair count, -# default 14) or via workflow_dispatch. Workload default: ethrex 100tx --continuations; -# "mono[TX]" = legacy monolithic prove. Cont proofs need rkyv pointer_width_64 on both -# sides, so PR branches older than that fix must rebase before a cont bench. +# Triggered by a "/bench-gpu [N] [real|cont[TX]|mono[TX]]" comment on a PR (N = pair +# count, default 14) or via workflow_dispatch. +# +# Workload default: the REAL BLOCK (--continuations). Unlike the CPU per-PR path, where +# the real block is opt-in because one shared runner serializes every bench in the repo, +# this workflow rents its own box per run β€” the cost is money, not queue time β€” so the +# representative workload is the default and the synthetic ones stay reachable for +# continuity: "cont[TX]" / "mono[TX]" select the N-transfer synthetic fixtures as before. +# Cont proofs need rkyv pointer_width_64 on both sides, so PR branches older than that +# fix must rebase before a cont bench. # Orchestration runs on a GitHub-hosted runner; all GPU work happens on the rented # Vast box (provisioned by the template onstart). # @@ -24,8 +30,8 @@ on: description: "Number of A/B/B/A pairs" default: "14" mode: - description: "Workload: cont[TX] (--continuations, TX defaults to 100) or mono[TX] (monolithic, TX defaults to 5)" - default: "cont100" + description: "Workload: real (default, real block + --continuations), cont[TX] (synthetic N-transfer + --continuations, TX defaults to 100), or mono[TX] (synthetic, monolithic, TX defaults to 5)" + default: "real" issue_comment: types: [created] @@ -93,15 +99,18 @@ jobs: ARGS="$DISPATCH_MODE" PAIRS=${DISPATCH_PAIRS:-14} fi - # Defaults: continuations with the 100-transfer fixture (production mode). - CONTINUATIONS=1; TX_COUNT=100 + # Default: the real block. Any cont/mono token switches to the synthetic + # fixtures at that transfer count, which is how pre-existing numbers stay + # reproducible with the exact syntax that produced them. + BENCH_WORKLOAD=real; CONTINUATIONS=1; TX_COUNT=100 set -f # tokens must not glob-expand against the runner's CWD for tok in $ARGS; do case "$tok" in - cont) CONTINUATIONS=1; TX_COUNT=100 ;; - mono) CONTINUATIONS=0; TX_COUNT=5 ;; - cont[0-9]*) CONTINUATIONS=1; TX_COUNT="${tok#cont}" ;; - mono[0-9]*) CONTINUATIONS=0; TX_COUNT="${tok#mono}" ;; + real) BENCH_WORKLOAD=real; CONTINUATIONS=1 ;; + cont) BENCH_WORKLOAD=synthetic; CONTINUATIONS=1; TX_COUNT=100 ;; + mono) BENCH_WORKLOAD=synthetic; CONTINUATIONS=0; TX_COUNT=5 ;; + cont[0-9]*) BENCH_WORKLOAD=synthetic; CONTINUATIONS=1; TX_COUNT="${tok#cont}" ;; + mono[0-9]*) BENCH_WORKLOAD=synthetic; CONTINUATIONS=0; TX_COUNT="${tok#mono}" ;; [0-9]*) PAIRS="$tok" ;; *) echo "::warning::ignoring unrecognized token '$tok'" ;; esac @@ -132,10 +141,12 @@ jobs: PAIRS=$((PAIRS + 1)) echo "::notice::rounded odd pair count up to $PAIRS so AB/BA orders balance" fi - if [ "$CONTINUATIONS" = "1" ]; then - WORKLOAD="ethrex ${TX_COUNT}tx continuations" + if [ "$BENCH_WORKLOAD" = "real" ]; then + WORKLOAD="ethrex real block, continuations" + elif [ "$CONTINUATIONS" = "1" ]; then + WORKLOAD="ethrex ${TX_COUNT}tx synthetic continuations" else - WORKLOAD="ethrex ${TX_COUNT}tx monolithic" + WORKLOAD="ethrex ${TX_COUNT}tx synthetic monolithic" fi # Outputs land before the fail-fast below so the always() result # comment is fully labeled even when this step exits early. @@ -146,6 +157,7 @@ jobs: echo "pairs=$PAIRS" echo "continuations=$CONTINUATIONS" echo "tx_count=$TX_COUNT" + echo "bench_workload=$BENCH_WORKLOAD" echo "workload=$WORKLOAD" } >> "$GITHUB_OUTPUT" # Fail fast if the PR side predates the pointer_width_64 fix: a >=40tx @@ -155,7 +167,7 @@ jobs: # status AND on the payload looking like the manifest (it declares # rkyv) β€” never treat an error blob as a missing feature. Purely # textual; deletable once every open branch postdates the fix. - if [ "$CONTINUATIONS" = "1" ] && [ "$TX_COUNT" -ge 40 ]; then + if [ "$BENCH_WORKLOAD" = "synthetic" ] && [ "$CONTINUATIONS" = "1" ] && [ "$TX_COUNT" -ge 40 ]; then REF="${OUT_HEAD_SHA:-$OUT_BRANCH}" if SIDE=$(gh api "repos/$GITHUB_REPOSITORY/contents/prover/Cargo.toml?ref=$REF" \ -H "Accept: application/vnd.github.raw" 2>/dev/null) \ @@ -173,6 +185,7 @@ jobs: env: PAIRS: ${{ steps.config.outputs.pairs }} WORKLOAD: ${{ steps.config.outputs.workload }} + BENCH_WORKLOAD: ${{ steps.config.outputs.bench_workload }} with: script: | await github.rest.reactions.createForIssueComment({ @@ -182,7 +195,14 @@ jobs: // Post the "started" notice under the SAME marker the result step uses, so the // result updates this comment in place (and re-runs reuse it rather than stacking). const marker = 'GPU Benchmark (ABBA)'; - const body = `## GPU Benchmark (ABBA) β€” running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. This takes ~2.5 hr at the default workload; the result will replace this comment.`; + // No GPU duration is quoted for the real block: we have never measured one, + // and the CPU rate does not transfer. The prior from the RTX 5090 sweep is that + // above epoch 2^21 the prover was CPU-bound at the serial producer, so real-block + // GPU time may land closer to CPU time than people expect. The first run sets it. + const eta = process.env.BENCH_WORKLOAD === 'real' + ? 'Runtime for the real block is UNMEASURED β€” this run establishes the baseline.' + : 'This takes ~2.5 hr at the synthetic 100tx workload.'; + const body = `## GPU Benchmark (ABBA) β€” running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. ${eta} The result will replace this comment.`; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, per_page: 100, @@ -234,10 +254,14 @@ jobs: # because vast can't numerically compare the driver_version string server-side. MIN_DRIVER: "580" run: | - # cpu_ram filter is in GB. Floor 48 GB: continuation proves are flat-memory - # (~10 GB) and the legacy 5tx monolithic prove also fits β€” far below the old - # 20-transfer prove (~78 GB heap) that set the previous 96 GB floor. 48 GB - # widens the dedicated pool (~15 vs ~11 offers). + # cpu_ram filter is in GB. Floor 48 GB, unchanged by the move to the real + # block: continuation peak heap is set by the EPOCH SIZE, not the block, so + # the real block costs the same ~10 GB working set at bench_abba.sh's default + # epoch 2^20, plus accumulated epoch proofs (~70 MB each, ~64 epochs) for a + # ~14 GB total β€” and ~18 GB if the epoch is ever raised to 2^21. Both sit far + # under this floor, which was already sized for the legacy 5tx monolithic + # prove. (The 20-transfer monolithic prove at ~78 GB heap set the previous + # 96 GB floor; nothing here goes monolithic on a real block.) # gpu_frac=1 requires a WHOLE-MACHINE offer (you rent every GPU on the host), so # Vast places no other tenant on the box: CPU cores, RAM/memory bandwidth, PCIe, # and NVMe are fully dedicated. Without it the "most expensive" sort below lands on @@ -397,6 +421,10 @@ jobs: PAIRS: ${{ steps.config.outputs.pairs }} CONTINUATIONS: ${{ steps.config.outputs.continuations }} TX_COUNT: ${{ steps.config.outputs.tx_count }} + # Only ever the literal "real" or "synthetic" β€” the config step's case arms + # cannot emit anything else β€” so it is safe to interpolate into the remote + # `bash -lc` below, the same discipline as the digits-only TX_COUNT/PAIRS. + BENCH_WORKLOAD: ${{ steps.config.outputs.bench_workload }} run: | SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST" @@ -437,7 +465,7 @@ jobs: git fetch --force origin main; $FETCH; \ git checkout -f origin/main; \ REBUILD=1 CUDARC_PIN=cuda-12080 SYSROOT_DIR=/opt/lambda-vm-sysroot BENCH_FEATURES='$BENCH_FEATURES' \ - CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT \ + WORKLOAD=$BENCH_WORKLOAD CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT \ scripts/bench_abba.sh $REF_A origin/main $PAIRS" # pipefail so a failed remote bench (e.g. a prove that dies) propagates through the diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index fd0e7ad3e..061c6e407 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -10,8 +10,9 @@ # than an unpaired two-sample test. # # WHAT IT DOES: -# 1. Builds the ethrex guest ELF + 5-transfer fixture once (identical for both -# sides β€” a prover-only change doesn't touch the guest). +# 1. Builds the ethrex guest ELF and obtains the workload fixture once (identical +# for both sides β€” a prover-only change doesn't touch the guest). Which fixture +# is WORKLOAD's business; see below. # 2. Builds the `cli` prover at REF_A and REF_B (skips the build and reuses the # cached binaries if they already exist; set REBUILD=1 to force). # 3. Runs N_PAIRS interleaved pairs in A B B A ... order (alternating which side @@ -35,6 +36,16 @@ # EPOCH_SIZE_LOG2= continuation epoch size (default 20; min 18). # TX_COUNT= ethrex transfer fixture to prove (default 5; use 20 for a # large continuation trace where GPU-residency wins are visible). +# Ignored when WORKLOAD=real. +# WORKLOAD=synthetic|real (default synthetic) which block to prove. `real` +# fetches the real-block fixture (block identity lives in the Makefile) and +# forces --continuations; TX_COUNT and CONTINUATIONS do not apply to it. +# +# On WORKLOAD: the synthetic default is N plain transfers β€” ecrecover-heavy over a +# near-empty state β€” while a real block is keccak- and trie-bound, so a prover change +# can move the two in opposite directions. `real` is what benchmark-gpu.yml runs by +# default; `synthetic` stays the default HERE so CPU /bench-abba results remain +# comparable with everything recorded before. # # Sizing (ethrex pair-noise sd ~1.2%, 80% power): ~12 pairs for a 1% effect, # ~18 for 0.8%, ~32 for 0.6%. Default 20 -> solid on 0.8-1%, ~60% power at 0.6% @@ -65,6 +76,18 @@ CONTINUATIONS="${CONTINUATIONS:-0}" EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-20}" # ethrex transfer-count fixture to prove (executor/tests/ethrex_${TX_COUNT}_transfers.bin). TX_COUNT="${TX_COUNT:-5}" +WORKLOAD="${WORKLOAD:-synthetic}" +case "$WORKLOAD" in + synthetic|real) ;; + *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;; +esac +# A real block sits far past the monolithic memory ceiling (~4.9 GB of peak heap per +# million cycles puts it in the hundreds of GB), so continuations are forced rather +# than offered: CONTINUATIONS=0 here could only produce an OOM. +if [ "$WORKLOAD" = "real" ] && [ "$CONTINUATIONS" != "1" ]; then + echo " NOTE: WORKLOAD=real always proves with continuations (monolithic would OOM)." + CONTINUATIONS=1 +fi if [ "$CONTINUATIONS" = "1" ]; then CONT_ARGS="--continuations --epoch-size-log2 $EPOCH_SIZE_LOG2" else @@ -72,7 +95,8 @@ else fi ELF_REL="executor/program_artifacts/rust/ethrex.elf" -INPUT_REL="executor/tests/ethrex_${TX_COUNT}_transfers.bin" +# Resolved after the cd to the repo root (WORKLOAD=real reads it from the Makefile). +INPUT_REL="" WORK="/tmp/abba_run" WT="/tmp/abba_wt" PROOF="/tmp/abba_proof.bin" @@ -94,6 +118,16 @@ if [ $((N_PAIRS % 2)) -ne 0 ]; then fi echo " pairs=$N_PAIRS (=$((N_PAIRS * 2)) prove runs)" +# The real block's identity lives in the Makefile and nowhere else, so repointing it +# never needs an edit here. +if [ "$WORKLOAD" = "real" ]; then + INPUT_REL="$(make -s print-real-block-fixture)" + echo " workload=real $INPUT_REL (continuations, epoch 2^$EPOCH_SIZE_LOG2)" +else + INPUT_REL="executor/tests/ethrex_${TX_COUNT}_transfers.bin" + echo " workload=synthetic ${TX_COUNT}tx $INPUT_REL" +fi + mkdir -p "$WORK" # --- 1. Guest ELF + fixture (identical for both sides; build once if missing) --- @@ -103,9 +137,17 @@ if [ ! -f "$ELF_REL" ]; then make "$ELF_REL" fi if [ ! -f "$INPUT_REL" ]; then - echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" - ( cd tooling/ethrex-fixtures && cargo build --release ) - tooling/ethrex-fixtures/target/release/ethrex-fixtures "$TX_COUNT" "$INPUT_REL" distinct + if [ "$WORKLOAD" = "real" ]; then + # ~1 MB, gitignored, never in a fresh checkout β€” and a rented GPU box is always a + # fresh checkout. Fetched by URL + sha256, not built: no converter, no ethrex host + # dependency tree, so this costs seconds on the box. + echo "==> Fetching ethrex real-block fixture (missing)" + make ethrex-real-block-fixture + else + echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" + ( cd tooling/ethrex-fixtures && cargo build --release ) + tooling/ethrex-fixtures/target/release/ethrex-fixtures "$TX_COUNT" "$INPUT_REL" distinct + fi fi ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")" INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 640e293d5..5d73978d5 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -134,11 +134,27 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~6 min | | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~6 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~30 min | +| `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | **unmeasured** (see below) | +| `scripts/bench_abba.sh` | `WORKLOAD=real scripts/bench_abba.sh ` | ~6 min Γ— 2 Γ— pairs | None of them hardcode the fixture path or a block number β€” they read the path from `make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when the `.bin` is absent. +**Why the GPU default differs from the CPU one.** `/bench-gpu` rents its own box per +run, so the cost is money rather than queue time on a runner every other bench is +waiting for. That removes the reason to keep the real block opt-in, so there it is +the default and the synthetic fixtures stay reachable via the existing `cont[TX]` / +`mono[TX]` tokens β€” old numbers reproduce with the exact syntax that produced them. +On the CPU side the shared runner makes the opposite trade correct. + +**GPU prove time for the real block is UNMEASURED.** Do not extrapolate the CPU rate +(5.31–5.62 s/Mcycle): it does not transfer. The relevant prior is the RTX 5090 sweep's +finding that above epoch 2^21 the prover was CPU-bound at the serial producer, which +means real-block GPU time may land closer to CPU time than expected rather than far +below it. The first `/bench-gpu` run establishes the baseline; treat any number quoted +before that as a guess. + **Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic prove grows ~4.9 GB per million cycles on this workload family (measured on the bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), From 111fc1fb020650e635658b786e5430867f7e23c0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 17:31:29 -0300 Subject: [PATCH 16/27] docs(ethrex): record /bench-abba's real-block cost and why it stays manual The knob exists (WORKLOAD=real, which benchmark-gpu.yml drives) but the CPU ABBA workflow keeps its synthetic default: 20 pairs x 2 proves x ~6 min is ~4 hours on the one shared bench runner that every other bench queues behind. Documented as an option to reach for deliberately rather than left as an undocumented capability. --- tooling/ethrex-real-block/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 5d73978d5..f7aef333f 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -135,12 +135,19 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~6 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~30 min | | `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | **unmeasured** (see below) | -| `scripts/bench_abba.sh` | `WORKLOAD=real scripts/bench_abba.sh ` | ~6 min Γ— 2 Γ— pairs | +| `scripts/bench_abba.sh` | `WORKLOAD=real scripts/bench_abba.sh ` | ~4 h at 20 pairs β€” **manual only** | None of them hardcode the fixture path or a block number β€” they read the path from `make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when the `.bin` is absent. +**`/bench-abba` is deliberately NOT wired to the real block.** The script supports +it (`WORKLOAD=real`, which is what the GPU workflow drives), but the CPU ABBA +workflow still defaults to the synthetic fixture: 20 pairs Γ— 2 proves Γ— ~6 min is +**~4 hours** on the one shared bench runner, which every other bench queues behind. +Run it by hand when a paired test on a real workload is worth that; the option is +here rather than a footgun on a comment trigger. + **Why the GPU default differs from the CPU one.** `/bench-gpu` rents its own box per run, so the cost is money rather than queue time on a runner every other bench is waiting for. That removes the reason to keep the real block opt-in, so there it is From c163766a452efd3fd564fb5c8b8fe31d413f8f25 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 18:17:35 -0300 Subject: [PATCH 17/27] docs(ethrex): refresh the measured numbers to main vintage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch predated #861 (thin LTO on the ethrex guest) and #863, so every cycle count and derived cost in these docs described a guest that no longer exists. Re-measured on this branch at the merge, guest rebuilt from source: real block 74,819,518 -> 50,781,557 cycles synthetic 9,063,727 -> 8,734,622 cycles ratio ~7x -> ~5.8x so the CPU prove drops ~6 min -> ~4.5-4.8 min and the monolithic-heap figure ~330 GB -> ~240 GB (re-derived from the same measured growth fit via a same-ELF cycle ratio, not rescaled by hand). Accelerator call counts are unchanged at 10,478 keccak / 116 ECSM, as expected β€” they follow the workload, not codegen. Every count now names the ELF it came from. Two things move them and both have bitten this doc already: guest optimisation (#861), and the clang major on PATH, worth ~2% β€” the Makefile pins the guest's target flags but not its compiler, so `cc` takes whatever clang is installed. The 50,713,534 the RTX 5090 box measured for this block on the same main commit is that effect, not a regression: clang 18 there, clang 21 here, 0.13% apart. GPU is no longer unmeasured: 59.87 s wall at --epoch-size-log2 22 on an RTX 5090, which is the recommended GPU epoch because VRAM binds β€” 2^22 leaves 28.9% headroom and 2^23 does not fit a 32 GiB card at all. CPU is ~4.5-4.8x that wall time. The CPU-server epoch recommendation is left as "pending calibration" rather than guessed from the GPU figure. The two alternate candidate blocks keep their PRE-LTO numbers, explicitly labelled and marked for re-measurement. Removing them would lose the selection evidence; presenting them next to a main-vintage number without a label would be exactly the vintage-mixing error the rest of this change is about. --- .github/workflows/benchmark-pr.yml | 27 +++--- scripts/bench_verify.sh | 4 +- scripts/perf_diff.sh | 14 ++-- tooling/ethrex-real-block/README.md | 124 +++++++++++++++++----------- 4 files changed, 101 insertions(+), 68 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 6d893496c..20699d275 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -45,19 +45,20 @@ env: # "Generate ethrex bench fixtures" step). # # This is not a representative block β€” 20 plain transfers are ecrecover-heavy and - # touch almost no state: 9.06M cycles, 411 keccak calls, 80 ecsm calls. It stays + # touch almost no state: 8.73M cycles, 411 keccak calls, 80 ecsm calls. It stays # the per-comment default only because it proves in ~25s; the representative # number is the real block below, which runs on push/dispatch and `/bench-real`. # - # Cycle counts here are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"). - # They move ~14% with guest-ELF vintage, so a count quoted against a different ELF - # will read as a regression β€” always pin the ELF when repeating one. + # Cycle counts here are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"), + # as of main @ 9ccdaf2 with clang 21. They move with guest optimisation (#861 gave the + # guest thin LTO) and ~2% with the clang major on PATH, so a count quoted against a + # different ELF reads as a regression β€” always pin the ELF when repeating one. ELF: executor/program_artifacts/rust/ethrex.elf INPUT: executor/tests/ethrex_bench_20.bin # Headline (representative) workload: a real Ethereum block. WHICH block lives in # the Makefile and nowhere else β€” nothing in this file names one, so a repoint - # moves this job without editing it. At the current default that is ~65.6M cycles, - # 10,478 keccak calls and 116 ecsm calls: ~7x the work of the synthetic block with a + # moves this job without editing it. At the current default that is 50.78M cycles, + # 10,478 keccak calls and 116 ecsm calls: ~5.8x the work of the synthetic block with a # ~18x different keccak:ecrecover mix. That is the whole point β€” a prover change # can move the synthetic number and the real one in opposite directions. # @@ -69,10 +70,10 @@ env: # Continuations are mandatory here, not a preference: a monolithic prove costs # ~4.9 GB of peak heap per million cycles on this workload family (from the # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so the - # current default would need ~330 GB and a heavier candidate up to ~700 GB. + # current default would need ~240 GB and a heavier candidate far more. # `--continuations` makes peak heap a function of the epoch size instead of the - # trace length: ~18 GB here (a 15.8 GB epoch-2^21 working set plus ~60 MB per - # epoch of accumulated proofs over ~32 epochs). Costs move with the block; the + # trace length: ~18 GB here (a 15.8 GB epoch-2^21 working set plus ~69 MB per + # epoch of accumulated proofs over ~25 epochs). Costs move with the block; the # per-candidate table is in tooling/ethrex-real-block/README.md. # # Budget ~2 GB of disk for the bundle each run β€” hence the `rm -f` after every @@ -182,8 +183,8 @@ jobs: # Real-block benchmark: same gating as growth (/bench-real, push to main, # workflow_dispatch), and for the same reason β€” it is far too slow for the - # per-comment tier. One continuation prove of the real block is ~6 min of - # CPU on this runner (~65.6M cycles at the 5.31-5.62 s/Mcycle measured on + # per-comment tier. One continuation prove of the real block is ~4.5-4.8 min of + # CPU on this runner (50.78M cycles at the 5.31-5.62 s/Mcycle measured on # this box in run 30631871174), against ~25s for the synthetic block, and # the runner is single and serialized across every /bench, /bench-abba and # /bench-verify in the repo. @@ -993,7 +994,7 @@ jobs: const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Synthetic screen β€” 20 plain transfers, ~7x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; + body += `Synthetic screen β€” 20 plain transfers, ~6x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Ξ” |\n`; body += `|--------|------|----|---|\n`; body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; @@ -1119,7 +1120,7 @@ jobs: // --- Footer --- if (!realTime) { body += `\n> 🧱 **This ran on synthetic blocks only.** Comment \`/bench-real\` to prove a real Ethereum block `; - body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~6 min to the run.\n`; + body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~5 min to the run.\n`; } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 85478bd4a..2cf34b40b 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -48,8 +48,8 @@ # trie-bound), so a verifier change can move the two differently. # # `synthetic` stays the default because it is what `/bench-verify` runs and what every -# number recorded so far used; `real` is the representative one, and costs a ~6 min -# continuation prove per side (cached in $WORK afterwards) before any verify run starts. +# number recorded so far used; `real` is the representative one, and costs a ~4.5-4.8 +# min continuation prove per side (cached in $WORK afterwards) before any verify run. # Both sides always prove the same block, so a comparison is never mixed. set -euo pipefail diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index ba9ca3619..1478b700e 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -17,14 +17,16 @@ # EPOCH_SIZE_LOG2= (default 21) sizes the epoch, WORKLOAD=real only. # # Pick the workload that matches the run you are localizing, because the symbol -# mix follows the block: the synthetic default is 20 plain transfers (9.06M cycles, -# 411 keccak calls, 80 ecsm calls) and a real block inverts that (~65.6M cycles, +# mix follows the block: the synthetic default is 20 plain transfers (8.73M cycles, +# 411 keccak calls, 80 ecsm calls) and a real block inverts that (50.78M cycles, # 10,478 keccak, 116 ecsm), so a hot symbol in one need not be hot in the other. -# Both counts are for a current guest ELF; they shift ~14% with ELF vintage. +# Both counts are from the same guest ELF (merge fdb92f67, main @ 9ccdaf2, clang 21); +# they move with guest optimisation (#861's thin LTO) and ~2% with the clang major, so +# pin the ELF when quoting one. # -# WORKLOAD=real also switches to a continuation prove (monolithic would need -# ~330 GB at that trace length), which is ~6 min per recording β€” five recordings, -# so budget ~30 min, plus ~2 GB of disk per bundle. +# WORKLOAD=real also switches to a continuation prove (monolithic would need ~240 GB +# at that trace length), which is ~4.5-4.8 min per recording β€” five recordings, so +# budget ~25 min, plus ~2 GB of disk per bundle. # # Produces: # - two perf-diff tables (recorded twice per side, interleaved B A B A β€” diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index f7aef333f..403f5ed1b 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -18,17 +18,26 @@ All are measured. | gas used | 420,000 | **2,428,684** | | transactions | 20 (all plain transfers) | 29 (real mix) | | serialized size | 32,766 B | 1,110,156 B | -| cycles | 9,063,727 | **~65.6M** | +| cycles | 8,734,622 | **50,781,557** | | keccak / ecsm calls | 411 / 80 | **10,478** / 116 | | keccaks per ecrecover | 5.1 | **90** | -Note the real block uses only ~5.8x the gas while costing ~7.2x the cycles, and -that it inverts the crypto mix: the synthetic block is ecrecover-bound, the real one -keccak- and trie-bound. That inversion is the entire point β€” a prover change can -move the two numbers in opposite directions. - -Cycle counts are for a **current guest ELF** and move ~14% with ELF vintage (this -block reads 74,819,518 on a mid-July ELF); pin the ELF whenever you quote one. +Note the real block uses ~5.8x the gas and costs ~5.8x the cycles here, and that it +inverts the crypto mix: the synthetic block is ecrecover-bound, the real one keccak- +and trie-bound. That inversion is the point β€” a prover change can move the two +numbers in opposite directions. (The gas and cycle ratios agreeing is a coincidence +of this pair, not a rule: cycles/gas is 20.9 here and ranges ~21–38 across the +candidate blocks.) + +**Pin the ELF whenever you quote a cycle count.** Counts above were measured on this +branch at merge `fdb92f67` (main @ `9ccdaf2`), guest built with **clang 21.1.8**. +Two things move them: +- **Guest optimisation.** #861 gave the guest thin LTO; this block read 74,819,518 + on a mid-July pre-LTO ELF, so anything quoting ~74.8M or ~65.6M is **superseded**. +- **clang major version**, by ~2%. The guest embeds C (secp256k1-sys) and the + Makefile pins target flags but not the compiler, so `cc` picks up whatever `clang` + is on PATH. The RTX 5090 box (clang-18) measured **50,713,534** for this block on + main @ `9ccdaf2` β€” 0.13% below the number above, same commit, different compiler. Why this block specifically: it was the **only** block in a 90-day Dune sweep that matched the shape constraints in the 1.6–2.6M gas band β€” exactly 2 heavy @@ -131,10 +140,10 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | Where | How to run it | Cost (current default) | |---|---|---| -| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~6 min | -| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~6 min per side, then cached | -| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~30 min | -| `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | **unmeasured** (see below) | +| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~4.5–4.8 min | +| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~4.5–4.8 min per side, then cached | +| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~25 min | +| `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | 59.87 s/prove on an RTX 5090 (see below) | | `scripts/bench_abba.sh` | `WORKLOAD=real scripts/bench_abba.sh ` | ~4 h at 20 pairs β€” **manual only** | None of them hardcode the fixture path or a block number β€” they read the path from @@ -143,8 +152,8 @@ the `.bin` is absent. **`/bench-abba` is deliberately NOT wired to the real block.** The script supports it (`WORKLOAD=real`, which is what the GPU workflow drives), but the CPU ABBA -workflow still defaults to the synthetic fixture: 20 pairs Γ— 2 proves Γ— ~6 min is -**~4 hours** on the one shared bench runner, which every other bench queues behind. +workflow still defaults to the synthetic fixture: 20 pairs Γ— 2 proves Γ— ~4.5–4.8 min +is **~3 hours** on the one shared bench runner, which every other bench queues behind. Run it by hand when a paired test on a real workload is worth that; the option is here rather than a footgun on a comment trigger. @@ -155,31 +164,36 @@ the default and the synthetic fixtures stay reachable via the existing `cont[TX] `mono[TX]` tokens β€” old numbers reproduce with the exact syntax that produced them. On the CPU side the shared runner makes the opposite trade correct. -**GPU prove time for the real block is UNMEASURED.** Do not extrapolate the CPU rate -(5.31–5.62 s/Mcycle): it does not transfer. The relevant prior is the RTX 5090 sweep's -finding that above epoch 2^21 the prover was CPU-bound at the serial producer, which -means real-block GPU time may land closer to CPU time than expected rather than far -below it. The first `/bench-gpu` run establishes the baseline; treat any number quoted -before that as a guess. +**GPU baseline (measured).** On an RTX 5090 (32 GiB) against main @ `9ccdaf2`, the +real block proves in **59.87 s wall at `--epoch-size-log2 22`** β€” the recommended GPU +epoch. VRAM is what binds: 2^22 leaves 28.9% headroom, and **2^23 does not fit a 32 GiB +card** (no card below 48 GiB can move up). The CPU bench runner is roughly **4.5–4.8x** +that wall time for the same block. + +Do not derive one from the other in general: the CPU rate (5.31–5.62 s/Mcycle) does not +transfer to the GPU, and the RTX 5090 sweep found the prover CPU-bound at the serial +producer above epoch 2^21, so GPU time lands closer to CPU time than a naive +device-throughput estimate suggests. **Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic prove grows ~4.9 GB per million cycles on this workload family (measured on the bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), -so this block would need **~330 GB** monolithically β€” and a heavier candidate up to -~700 GB. `--continuations` makes peak heap a function of the epoch size instead of -the trace length, so the same block fits in **~18 GB** at epoch 2^21: a 15.8 GB -epoch working set plus ~60 MB per epoch of accumulated proofs over ~32 epochs. The +so this block would need **~240 GB** monolithically β€” and a heavier candidate far +more. `--continuations` makes peak heap a function of the epoch size instead of the +trace length, so the same block fits in **~18 GB** at epoch 2^21: a 15.8 GB epoch +working set plus ~69 MB per epoch of accumulated proofs over ~25 epochs. (The GPU path +uses epoch 2^22, where VRAM rather than host RAM is the binding constraint.) The bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64` to serialize. Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in -~25s against ~6 min, and one runner carries every `/bench`, `/bench-abba` and +~25s against ~4.5–4.8 min, and one runner carries every `/bench`, `/bench-abba` and `/bench-verify` in the repo. The synthetic number is a fast screen; the real block is the number that means something. -Cycle counts here (9.06M synthetic, ~65.6M real) are for a **current guest ELF**. -They move ~14% with ELF vintage β€” this block reads 74,819,518 on a mid-July ELF β€” so -pin the ELF whenever you quote one, or it will look like a regression the next time +Cycle counts here (8.73M synthetic, 50.78M real) are from merge `fdb92f67` (main @ +`9ccdaf2`, clang 21). They move with guest optimisation and ~2% with the clang major, +so pin the ELF whenever you quote one, or it will look like a regression the next time someone measures. ## Where validation runs @@ -257,27 +271,43 @@ repointed ~1 MB fixture cannot become committable by accident. ### Measured cost of candidate blocks -Cost is a property of the block, so it changes with the repoint. Cycles are given -for a **current guest ELF**; prove time and heap are for the CPU bench runner at -epoch 2^21, using the 5.31–5.62 s per Mcycle measured on that box. - -| block | gas | cycles | prove | peak heap | proof | fixture | -|---|---|---|---|---|---|---| -| **mainnet 25368371** β€” *current default* | 2.43M | **~65.6M** | **~6 min** | ~18 GB | ~1.9 GB | 1,110,156 B, `61eba49b…` | -| mainnet 25453112 | 4.24M | ~110M | ~10 min | ~19 GB | ~3.7 GB | 2,019,747 B, `0298663d…` | -| hoodi 1265656 | 4.40M | ~147.5M | ~13 min | ~21 GB | ~4.7 GB | 1,021,207 B, `1f7d4c4c…` | - -These are measurements, not estimates. All three clear the usability screen. Add a -row rather than editing the wiring. +Cost is a property of the block, so it changes with the repoint. All figures are +measured, never derived from gas β€” **cycles per gas is not constant** (20.9 for the +current default), so ranking candidates by gas mispredicts cost. -Two things the table shows that a gas-based estimate would have got wrong. **Cycles -per gas is not constant** β€” it ranges ~26–38 across these blocks β€” so ranking -candidates by gas mispredicts cost; 25453112 has *more* gas than hoodi 1265656 yet -costs ~25% fewer cycles. And **fixture size does not track cost** either: the current -default is the cheapest block and the middle-sized fixture. +**Current default β€” main-vintage (merge `fdb92f67`, main @ `9ccdaf2`):** -The default is the cheapest of the three, which matters because this workload sits on -a single shared bench runner. It was also the only block in a 90-day Dune sweep +| block | gas | cycles | GPU prove (RTX 5090) | CPU prove | proof | fixture | +|---|---|---|---|---|---|---| +| **mainnet 25368371** | 2.43M | **50,781,557** (clang 21)
50,713,534 (clang 18) | **59.87 s** @ epoch 2^22 | ~4.5–4.8x the GPU wall | ~1.9 GB | 1,110,156 B, `61eba49b…` | + +Epoch 2^22 is the GPU recommendation: VRAM binds, 2^22 leaves 28.9% headroom on a +32 GiB card and 2^23 does not fit one at all. **The CPU-server epoch recommendation is +pending calibration** β€” that measurement is still running; do not fill this in by +analogy with the GPU number. + +**Alternates β€” PRE-LTO vintage, superseded, re-measure before quoting.** These were +taken on a mid-July guest ELF, before #861 gave the guest thin LTO; the same build +change took the current default from 74,819,518 to ~50.7M, so expect these to fall by +a comparable factor. Kept because they are the selection evidence, not because the +numbers are current: + +| block | gas | cycles (pre-LTO) | fixture | +|---|---|---|---| +| mainnet 25453112 | 4.24M | 125,932,956 | 2,019,747 B, `0298663d…` | +| hoodi 1265656 | 4.40M | 168,319,360 | 1,021,207 B, `1f7d4c4c…` | + +All three clear the usability screen. Add a row rather than editing the wiring, and +say which ELF a number came from. + +Two things these numbers show that a gas-based estimate would have got wrong, and both +survive the vintage change because they are same-ELF comparisons. **Gas does not order +cost:** 25453112 carries *more* gas than hoodi 1265656 yet costs ~25% fewer cycles. +And **fixture size does not track cost** either: the current default is the cheapest +block and the middle-sized fixture. + +The default is the cheapest of the three, which matters because the CPU workload sits +on a single shared bench runner. It was also the only block in a 90-day Dune sweep matching the shape constraints (2 heavy transactions, no whale, sane transfer share) in the 1.6–2.6M gas band β€” so it is cheap *and* structurally typical, not cheap because it is degenerate. From 4ebd83603c0eabcdce872b26a3885f0f36828c1d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 18:34:16 -0300 Subject: [PATCH 18/27] bench(gpu): default the real-block GPU path to epoch 2^22 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the RTX 5090 calibration on 2026-07-31 against main @9ccdaf2 (raw traces in ~/workspace/lambda_vm_bench_cache/gpu_epoch_calib_2026-07-31/), same fixture and CLI, one prove per setting: 2^21 70.52 s 19,193 MiB VRAM (58.9%) 25 epochs 1.65 GB proof 2^22 59.87 s 23,193 MiB VRAM (71.1%) 13 epochs 1.12 GB proof 2^23 OOM at 32,079 MiB (98.4%) after 9.7 s β€” needs ~44 GiB VRAM is the binding constraint, so 2^22 is the largest setting that fits a 32 GiB card, and it is ~15% faster than 2^21 with 28.9% headroom left. 2^23 is unreachable for any card below 48 GiB. Scoped to the GPU flow. bench_abba.sh keeps its 2^20 default for the CPU /bench-abba, and the CLI's DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2 stays 20: 2^22 needs ~28 GiB of HOST memory on a CPU build and would break laptops. VRAM binds on one path and host RAM on the other β€” not the same question, so not the same default. The CPU-server recommendation is left explicitly pending calibration. Applied to the real-block path only, not to the synthetic cont[TX] tokens. Those exist so a GPU number recorded before the real-block migration still reproduces with the syntax that produced it, and silently re-pointing their epoch would break exactly that. mono[TX] has no epoch. --- .github/workflows/benchmark-gpu.yml | 30 ++++++++++++++++++++++++++++- tooling/ethrex-real-block/README.md | 29 +++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index c6de983d8..cb8768a23 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -53,6 +53,25 @@ env: VAST_IMAGE_DISK: "64" # cli features for the ABBA build β€” the GPU (cuda) prover path plus jemalloc heap stats. BENCH_FEATURES: "jemalloc-stats,prover/cuda" + # Continuation epoch for the REAL-BLOCK path, from the RTX 5090 calibration on + # 2026-07-31 against main @9ccdaf2 (raw traces: + # ~/workspace/lambda_vm_bench_cache/gpu_epoch_calib_2026-07-31/, PROVENANCE.txt). + # Measured on the 32,607 MiB card, same fixture and CLI, one prove per setting: + # + # 2^21 70.52 s wall 19,193 MiB VRAM (58.9%) 25 epochs 1.65 GB proof + # 2^22 59.87 s wall 23,193 MiB VRAM (71.1%) 13 epochs 1.12 GB proof + # 2^23 OOM at 32,079 MiB (98.4%) after 9.7 s β€” needs ~44 GiB + # + # So 2^22 is the largest setting that fits a 32 GiB card, and it is ~15% faster than + # 2^21 (equivalently, 2^21 is ~18% slower) with 28.9% VRAM headroom left. 2^23 is out + # of reach for every card below 48 GiB, not just this one. + # + # GPU PATH ONLY, and deliberately so. It is NOT pushed into bench_abba.sh's default + # (2^20, which the CPU /bench-abba uses) nor into the CLI's + # DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2 (also 20): 2^22 needs ~28 GiB of HOST memory on + # a CPU build, which would break laptops. VRAM is the binding constraint here and host + # RAM is the binding constraint there, so the two defaults are not the same question. + GPU_REAL_EPOCH_LOG2: "22" # Unique per-run label set on the instance, for easy identification in the Vast console. RUN_LABEL: "gpu-bench-${{ github.run_id }}-${{ github.run_attempt }}" # Pin the Vast CLI to an immutable commit (a PyPI version can be re-published; a commit @@ -388,6 +407,15 @@ jobs: KEY: ${{ steps.sshkey.outputs.key_path }} run: | SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST" + + # The calibrated epoch applies to the real block only. The synthetic cont[TX] + # tokens keep bench_abba.sh's 2^20 so a number recorded before this change still + # reproduces with the syntax that produced it β€” that reproducibility is the whole + # reason those tokens were kept. mono[TX] has no epoch at all. + EPOCH_ENV="" + if [ "$BENCH_WORKLOAD" = "real" ]; then + EPOCH_ENV="EPOCH_SIZE_LOG2=$GPU_REAL_EPOCH_LOG2" + fi echo "Waiting for the template onstart script to finish (Rust + LLVM + sysroot + clone)..." # The bootstrap's final stdout line is "=== done ===". Vast captures onstart # output to /var/log/onstart.log; fall back to checking the artifacts it leaves. @@ -465,7 +493,7 @@ jobs: git fetch --force origin main; $FETCH; \ git checkout -f origin/main; \ REBUILD=1 CUDARC_PIN=cuda-12080 SYSROOT_DIR=/opt/lambda-vm-sysroot BENCH_FEATURES='$BENCH_FEATURES' \ - WORKLOAD=$BENCH_WORKLOAD CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT \ + WORKLOAD=$BENCH_WORKLOAD CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT $EPOCH_ENV \ scripts/bench_abba.sh $REF_A origin/main $PAIRS" # pipefail so a failed remote bench (e.g. a prove that dies) propagates through the diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 403f5ed1b..db115b373 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -164,11 +164,30 @@ the default and the synthetic fixtures stay reachable via the existing `cont[TX] `mono[TX]` tokens β€” old numbers reproduce with the exact syntax that produced them. On the CPU side the shared runner makes the opposite trade correct. -**GPU baseline (measured).** On an RTX 5090 (32 GiB) against main @ `9ccdaf2`, the -real block proves in **59.87 s wall at `--epoch-size-log2 22`** β€” the recommended GPU -epoch. VRAM is what binds: 2^22 leaves 28.9% headroom, and **2^23 does not fit a 32 GiB -card** (no card below 48 GiB can move up). The CPU bench runner is roughly **4.5–4.8x** -that wall time for the same block. +**GPU baseline (measured), and why the GPU epoch is 2^22.** On an RTX 5090 (32,607 MiB) +against main @ `9ccdaf2`, same fixture and CLI, one prove per setting: + +| epoch | wall | VRAM | epochs | proof | +|---|---|---|---|---| +| 2^21 | 70.52 s | 19,193 MiB (58.9%) | 25 | 1.65 GB | +| **2^22** | **59.87 s** | 23,193 MiB (71.1%) | 13 | 1.12 GB | +| 2^23 | OOM after 9.7 s | 32,079 MiB (98.4%) β€” needs ~44 GiB | β€” | β€” | + +**VRAM is the binding constraint**, so 2^22 is simply the largest setting that fits a +32 GiB card β€” and it is ~15% faster than 2^21 (equivalently, 2^21 is ~18% slower) with +28.9% headroom to spare. 2^23 is out of reach for every card below 48 GiB, not just +this one. `benchmark-gpu.yml` defaults the real-block path to 2^22 for this reason; +raw traces are in `~/workspace/lambda_vm_bench_cache/gpu_epoch_calib_2026-07-31/` +(`PROVENANCE.txt`). + +That default is **GPU-path only**. `bench_abba.sh` still defaults to 2^20 for the CPU +`/bench-abba`, and the CLI's own `DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2` is still 20: +2^22 needs ~28 GiB of *host* memory on a CPU build, which would break laptops. VRAM +binds on one path and host RAM on the other, so they are not the same question and the +GPU number must not be copied across. **The CPU-server epoch recommendation is pending +calibration.** + +The CPU bench runner is roughly **4.5–4.8x** the GPU wall time for the same block. Do not derive one from the other in general: the CPU rate (5.31–5.62 s/Mcycle) does not transfer to the GPU, and the RTX 5090 sweep found the prover CPU-bound at the serial From 96ee504f5ca62458cf9431f0fc4c86844ee39ca5 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 18:55:35 -0300 Subject: [PATCH 19/27] =?UTF-8?q?bench(cpu):=20encode=20the=20CPU=20epoch?= =?UTF-8?q?=20calibration=20=E2=80=94=20bench=20runner=20to=202^22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CPU sweep landed (2026-07-31, 124 GiB / 32-core box, real block, branch vintage at 53,757,588 cycles): 2^20 52 ep 616.90 s 14.56 GiB RSS 2.83 GB proof 2^21 26 ep 464.26 s 18.43 GiB 1.72 GB 2^22 13 ep 397.88 s 32.21 GiB 1.15 GB 2^23 7 ep 356.47 s 60.01 GiB 0.90 GB 2^24 4 ep ~334 s ~97-105 GiB (provisional) There is a real knee: gain per doubling shrinks 24.7 -> 14.3 -> 10.4 -> ~6% while memory roughly doubles at each step near the top. So the choice is per-machine, and this commit encodes three tiers rather than one number: GPU 32 GiB card 2^22 (VRAM-bound; already set in benchmark-gpu.yml) CPU bench runner 2^22 (host-RAM-bound: 32.2 GiB fits a >=64 GiB box with ~50% headroom, 2^23's 60 GiB does not) CPU 128 GiB class 2^23 (the knee; 48% of a 124 GiB box) laptops / CLI 2^20 (unchanged) /bench-real moves 2^21 -> 2^22, worth ~14% wall. NOT ~35% β€” that figure is the 2^20 -> 2^22 gap, and this path was never at the CLI default; it has been explicitly 2^21 since it was wired. scripts/perf_diff.sh moves the same way, since it targets the same bench server and its real-block epoch should not disagree with /bench-real's. Absolute seconds from that table are deliberately not copied into any estimate: the calibration box runs ~8.6 s/Mcycle against this runner's 5.31-5.62, so the ratios between epochs transfer and the wall times do not. The docs say the first /bench-real run at 2^22 establishes the runner's real number. Left alone on purpose: the CLI's DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2 (20, so a laptop can still prove), bench_abba.sh's 2^20 (CPU /bench-abba), and bench_verify.sh's CONT_EPOCH_LOG2 (20, chosen by #878 for its SYNTHETIC arm to match bench_abba's bundle shape) β€” that one gains a pointer to the tier table for anyone running its real-block arm by hand. 2^24 fits a 124 GiB box but buys ~6% for ~15% headroom, so it is documented as "fits, marginal, not recommended on shared boxes" rather than adopted. Its RSS cell is marked provisional pending the calibration agent's formal report. --- .github/workflows/benchmark-pr.yml | 27 ++++++++++++--- scripts/bench_verify.sh | 6 +++- scripts/perf_diff.sh | 13 ++++--- tooling/ethrex-real-block/README.md | 53 +++++++++++++++++++++++++---- 4 files changed, 82 insertions(+), 17 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 20699d275..502f6e181 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -72,14 +72,31 @@ env: # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so the # current default would need ~240 GB and a heavier candidate far more. # `--continuations` makes peak heap a function of the epoch size instead of the - # trace length: ~18 GB here (a 15.8 GB epoch-2^21 working set plus ~69 MB per - # epoch of accumulated proofs over ~25 epochs). Costs move with the block; the - # per-candidate table is in tooling/ethrex-real-block/README.md. + # trace length. Costs move with the block; the per-candidate table is in + # tooling/ethrex-real-block/README.md. # - # Budget ~2 GB of disk for the bundle each run β€” hence the `rm -f` after every + # Budget ~1.2 GB of disk for the bundle each run β€” hence the `rm -f` after every # prove. A heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64`; # a PR branch predating that fix fails at write time rather than mismeasuring. - REAL_BLOCK_EPOCH_LOG2: "21" + # + # Epoch 2^22, from the CPU sweep on 2026-07-31 (124 GiB / 32-core box, real block, + # branch vintage; full table in tooling/ethrex-real-block/README.md): + # + # 2^21 464.26 s 18.43 GiB RSS 26 epochs 1.72 GB proof + # 2^22 397.88 s 32.21 GiB RSS 13 epochs 1.15 GB proof + # 2^23 356.47 s 60.01 GiB RSS 7 epochs 0.90 GB proof + # + # This runner's floor is >=64 GiB, so 2^22's 32.2 GiB fits with ~50% headroom while + # 2^23's 60 GiB does not fit safely β€” memory, not speed, is what picks 2^22 here. + # Moving off 2^21 is worth ~14% wall on the calibration box. That box is NOT this + # runner (it measured 8.6 s/Mcycle against this runner's 5.3-5.6), so treat the + # ratio as transferable and the absolute seconds as not: the first /bench-real run + # establishes this runner's actual time. + # + # Deliberately NOT the CLI's DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2, which stays 20 so + # a laptop can still prove; and not the GPU path's 2^22, which happens to coincide + # but is chosen by VRAM rather than host RAM (see benchmark-gpu.yml). + REAL_BLOCK_EPOCH_LOG2: "22" BENCH_RUNS_REAL: 1 BENCH_RUNS_PR: 3 # Cheap-tier screen: catches regressions down to ~1.5% on its own and leaves diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 2cf34b40b..e6d48bfcf 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -31,7 +31,11 @@ # `real` fetches the real-block fixture (identity lives in the Makefile) and runs # the continuation arm ONLY β€” a real block is hundreds of GB monolithically, so # that arm is skipped rather than left to OOM. See "Workload" below. -# CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). 20 matches +# CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). For +# WORKLOAD=real prefer the calibrated tier for the box you are on β€” 2^22 on the +# bench runner or a 64 GiB machine, 2^23 on a 128 GiB one (see +# tooling/ethrex-real-block/README.md, "Choosing the epoch size"); the default +# below is chosen for the SYNTHETIC arm. 20 matches # scripts/bench_abba.sh, so this arm proves the same bundle shape /bench already # proves on the same server β€” and 20 txs at 2^20 is strictly cheaper than the # 100 txs at 2^20 that /bench runs by default, so it can't be the thing that OOMs diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 1478b700e..62df16ded 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -14,7 +14,9 @@ # USAGE (on the bench server): # scripts/perf_diff.sh REF_A [REF_B=origin/main] # Env: WORKLOAD=synthetic|real (default synthetic) picks the block to profile; -# EPOCH_SIZE_LOG2= (default 21) sizes the epoch, WORKLOAD=real only. +# EPOCH_SIZE_LOG2= (default 22) sizes the epoch, WORKLOAD=real only. +# 22 is the calibrated bench-runner tier, matching /bench-real; use 23 on a +# 128 GiB box (tooling/ethrex-real-block/README.md, "Choosing the epoch size"). # # Pick the workload that matches the run you are localizing, because the symbol # mix follows the block: the synthetic default is 20 plain transfers (8.73M cycles, @@ -25,8 +27,8 @@ # pin the ELF when quoting one. # # WORKLOAD=real also switches to a continuation prove (monolithic would need ~240 GB -# at that trace length), which is ~4.5-4.8 min per recording β€” five recordings, so -# budget ~25 min, plus ~2 GB of disk per bundle. +# at that trace length), which is ~4-5 min per recording β€” five recordings, so budget +# ~25 min, plus ~1.2 GB of disk per bundle and ~32 GiB of RAM at the default epoch. # # Produces: # - two perf-diff tables (recorded twice per side, interleaved B A B A β€” @@ -45,7 +47,10 @@ fi REF_A="$1" REF_B="${2:-origin/main}" WORKLOAD="${WORKLOAD:-synthetic}" -EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-21}" +# 2^22: the calibrated tier for the bench server this script targets, same as +# /bench-real. Memory picks it, not speed β€” 32.2 GiB fits a >=64 GiB box with ~50% +# headroom where 2^23's 60 GiB does not. +EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-22}" case "$WORKLOAD" in synthetic|real) ;; *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;; diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index db115b373..b4125b1d9 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -140,7 +140,7 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | Where | How to run it | Cost (current default) | |---|---|---| -| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~4.5–4.8 min | +| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~4–5 min (first run at epoch 2^22 confirms) | | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~4.5–4.8 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~25 min | | `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | 59.87 s/prove on an RTX 5090 (see below) | @@ -199,9 +199,10 @@ prove grows ~4.9 GB per million cycles on this workload family (measured on the bench server: `10,728 MB + 2,007 MB/transfer`, RΒ² = 0.998 across 4β†’20 transfers), so this block would need **~240 GB** monolithically β€” and a heavier candidate far more. `--continuations` makes peak heap a function of the epoch size instead of the -trace length, so the same block fits in **~18 GB** at epoch 2^21: a 15.8 GB epoch -working set plus ~69 MB per epoch of accumulated proofs over ~25 epochs. (The GPU path -uses epoch 2^22, where VRAM rather than host RAM is the binding constraint.) The +trace length, so the same block fits in **~32 GiB** at epoch 2^22, the setting both the +CPU bench runner and the GPU path now use β€” picked by host RAM on one and by VRAM on +the other. See [Choosing the epoch size](#choosing-the-epoch-size) for the full curve +and the other tiers. The bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64` to serialize. @@ -301,9 +302,8 @@ current default), so ranking candidates by gas mispredicts cost. | **mainnet 25368371** | 2.43M | **50,781,557** (clang 21)
50,713,534 (clang 18) | **59.87 s** @ epoch 2^22 | ~4.5–4.8x the GPU wall | ~1.9 GB | 1,110,156 B, `61eba49b…` | Epoch 2^22 is the GPU recommendation: VRAM binds, 2^22 leaves 28.9% headroom on a -32 GiB card and 2^23 does not fit one at all. **The CPU-server epoch recommendation is -pending calibration** β€” that measurement is still running; do not fill this in by -analogy with the GPU number. +32 GiB card and 2^23 does not fit one at all. See +[Choosing the epoch size](#choosing-the-epoch-size) for the CPU tiers. **Alternates β€” PRE-LTO vintage, superseded, re-measure before quoting.** These were taken on a mid-July guest ELF, before #861 gave the guest thin LTO; the same build @@ -331,6 +331,45 @@ matching the shape constraints (2 heavy transactions, no whale, sane transfer sh in the 1.6–2.6M gas band β€” so it is cheap *and* structurally typical, not cheap because it is degenerate. +### Choosing the epoch size + +`--epoch-size-log2` trades memory for speed, and **the right value is a property of the +machine, not of the block**. Three tiers, all measured: + +| where | epoch | why | +|---|---|---| +| GPU, 32 GiB card | **2^22** | VRAM-bound β€” 2^23 does not fit | +| CPU bench runner (β‰₯64 GiB) | **2^22** | host-RAM-bound β€” 2^23's 60 GiB does not fit safely | +| CPU server, 128 GiB class | **2^23** | the knee; 2^24 fits but is not worth it | +| laptops (CLI default) | **2^20** | unchanged, so a plain `cli prove` still works | + +CPU sweep, 2026-07-31, on a 124 GiB / 32-core box, real block, **branch vintage** +(53,757,588 cycles on that box's clang-21 pre-LTO ELF): + +| epoch | epochs | wall | peak RSS | proof | +|---|---|---|---|---| +| 2^20 | 52 | 616.90 s | 14.56 GiB | 2.83 GB | +| 2^21 | 26 | 464.26 s | 18.43 GiB | 1.72 GB | +| 2^22 | 13 | 397.88 s | 32.21 GiB | 1.15 GB | +| **2^23** | 7 | **356.47 s** | **60.01 GiB** | 0.90 GB | +| 2^24 | 4 | ~334 s | ~97–105 GiB *(provisional)* | β€” | + +**There is a real knee.** Speed gained per doubling shrinks β€” 24.7%, 14.3%, 10.4%, +~6% β€” while memory roughly doubles at each step near the top. 2^23 uses 48% of a +124 GiB box (~52% headroom); 2^24 buys only ~6% more speed for ~15% headroom, so it +**fits but is not recommended on a shared box**, where one co-tenant turns a tight fit +into an OOM. The 2^24 RSS figure is provisional pending the calibration agent's formal +report. + +A main-vintage anchor also ran on the same box: 2^23 = 344.12 s / 58.87 GiB, i.e. ~3% +faster than branch vintage at the same memory β€” as expected, since #861 cut cycles and +peak RSS is set by the epoch size rather than the trace length. + +**Do not read absolute seconds off this table for another machine.** The calibration box +runs ~8.6 s/Mcycle against the bench runner's 5.31–5.62, so the *ratios* between epochs +transfer and the wall times do not. `/bench-real` now runs at 2^22 (it was 2^21); the +first run after that change establishes the runner's real number. + ### Verifying a repoint Run **both**, in this order: From bd60a613590b57a17d6ae95cb8385db23fb19d76 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:08:14 -0300 Subject: [PATCH 20/27] bench(ci): fold the real block into /bench, sampled at 3 runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/bench-real` is gone. The real block now runs on every invocation of plain `/bench`, alongside the synthetic screen rather than instead of it, and push-to-main publishes a real-block baseline the same way it publishes the synthetic one. One command instead of two, for the number that actually means something. Sampled 3x and summarised exactly like the synthetic screen β€” median headline, spread, and every raw value β€” so a single slow run shows up as spread instead of silently moving the verdict. The verdict bands are wider than the synthetic 5%: 3 runs of a minutes-long prove resolve less than 5 runs of a 25s one, so anything from 3-10% is reported as unresolved rather than as "fine". The baseline fallback path samples to the same count. A 3-vs-1 comparison would put the two sides' noise on different footings, which is the error sampling exists to avoid. Cost, stated plainly because it lands on a shared resource: one runner carries every /bench, /bench-abba and /bench-verify in the repo, and this takes a /bench from about a minute of proving to ~15-20 min, on every comment and every push to main. That trade is deliberate. BENCH_RUNS_REAL is the dial, and the comment next to it says so β€” the runner's per-run time is still unmeasured (the epoch calibration ran on a different box), so if runs land above ~6 min each, turning 3 down is the first move rather than the last resort. Checked rather than assumed: the proof bundle (~1.15 GB at epoch 2^22) is deleted between every run on both the PR and baseline loops, so 3 runs do not accumulate ~3.5 GB of disk; and peak heap is ~32 GiB against a runner floor of >=64 GiB with nothing else scheduled, since the runner serialises. The block-identity guard is untouched: a baseline that measured a different block still refuses to diff. Renderer: run count moved into the heading ("median of 3") to match the synthetic section instead of being repeated in the subtitle, and the footer no longer offers a command to request the real block β€” its absence now means the fixture could not be fetched, so it says that and points at the URL. Exercised over 8 scenarios including tight-spread, bad-spread, and fixture-unavailable. --- .github/workflows/benchmark-pr.yml | 150 ++++++++++++++++++---------- scripts/perf_diff.sh | 4 +- tooling/ethrex-real-block/README.md | 23 +++-- 3 files changed, 117 insertions(+), 60 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 502f6e181..55c5e674a 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -47,7 +47,7 @@ env: # This is not a representative block β€” 20 plain transfers are ecrecover-heavy and # touch almost no state: 8.73M cycles, 411 keccak calls, 80 ecsm calls. It stays # the per-comment default only because it proves in ~25s; the representative - # number is the real block below, which runs on push/dispatch and `/bench-real`. + # number is the real block below, which runs on every invocation alongside it. # # Cycle counts here are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"), # as of main @ 9ccdaf2 with clang 21. They move with guest optimisation (#861 gave the @@ -90,14 +90,23 @@ env: # 2^23's 60 GiB does not fit safely β€” memory, not speed, is what picks 2^22 here. # Moving off 2^21 is worth ~14% wall on the calibration box. That box is NOT this # runner (it measured 8.6 s/Mcycle against this runner's 5.3-5.6), so treat the - # ratio as transferable and the absolute seconds as not: the first /bench-real run - # establishes this runner's actual time. + # ratio as transferable and the absolute seconds as not: the first /bench run at + # this epoch establishes this runner's actual time. # # Deliberately NOT the CLI's DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2, which stays 20 so # a laptop can still prove; and not the GPU path's 2^22, which happens to coincide # but is chosen by VRAM rather than host RAM (see benchmark-gpu.yml). REAL_BLOCK_EPOCH_LOG2: "22" - BENCH_RUNS_REAL: 1 + # Sampled like the synthetic screen, 3 runs, and reported the same way (median + + # spread). Fewer than the synthetic's 5 because each run is minutes rather than + # seconds; enough that one slow run shows up as spread instead of moving the median. + # + # THIS IS THE DIAL. The runner's per-run real-block time is still unmeasured β€” the + # calibration ran on a different box (see REAL_BLOCK_EPOCH_LOG2 above). If the first + # runs land above ~6 min each, /bench becomes a ~25 min occupancy of a runner every + # other bench queues behind, and this count is what to turn down (2, or 1) before + # reaching for anything else. + BENCH_RUNS_REAL: 3 BENCH_RUNS_PR: 3 # Cheap-tier screen: catches regressions down to ~1.5% on its own and leaves # smaller/ambiguous deltas to the manual drift-free ABBA tiebreaker. Pushing either @@ -114,9 +123,10 @@ jobs: benchmark: runs-on: [self-hosted, bench] # Skip unless: push to main, workflow_dispatch, or "/bench" comment on a PR. - # "/bench-real" and "/bench-growth" are handled by THIS job (they are prefixed - # by "/bench" and deliberately absent from the exclusion list below); they only - # switch which sections run, in the "Determine run count" step. + # "/bench-growth" is handled by THIS job (it is prefixed by "/bench" and + # deliberately absent from the exclusion list below); it only switches whether the + # growth sweep runs, in the "Determine run count" step. The real block needs no + # token β€” it runs on every invocation. if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || @@ -198,22 +208,17 @@ jobs: echo "run_growth=false" >> "$GITHUB_OUTPUT" fi - # Real-block benchmark: same gating as growth (/bench-real, push to main, - # workflow_dispatch), and for the same reason β€” it is far too slow for the - # per-comment tier. One continuation prove of the real block is ~4.5-4.8 min of - # CPU on this runner (50.78M cycles at the 5.31-5.62 s/Mcycle measured on - # this box in run 30631871174), against ~25s for the synthetic block, and - # the runner is single and serialized across every /bench, /bench-abba and - # /bench-verify in the repo. + # The real block runs on EVERY invocation β€” plain `/bench`, push to main, and + # workflow_dispatch alike. It is the number that means something, so it should + # not need a second command to ask for; the synthetic screen rides along + # unchanged as the cheap, high-sensitivity half. # - # Because push-to-main always runs it, the baseline artifact carries a - # real-block number, so `/bench-real` on a PR pays for the PR side only. - RUN_REAL=false - if [ "$EVENT_NAME" = "issue_comment" ] && echo "$COMMENT_BODY" | grep -q '^/bench-real'; then - RUN_REAL=true - elif [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then - RUN_REAL=true - fi + # The cost is real and lands on a shared runner: ~4.5-4.8 min per run x + # BENCH_RUNS_REAL, so every /bench and every push to main now occupies the + # bench server for roughly 15-20 min, and /bench-abba and /bench-verify queue + # behind it. That trade was made deliberately β€” see BENCH_RUNS_REAL above for + # the dial if it proves too expensive. + RUN_REAL=true # The fixture is FETCHED from a pinned URL + sha256, not built (see the # Makefile). The guard covers the window after a repoint but before the new @@ -466,14 +471,24 @@ jobs: EPOCHS="$E" done + # Same summary as the synthetic screen: median for the headline, spread so a + # single slow run is visible rather than silently shifting the verdict, and + # every raw value so a reader can judge for themselves. MEDIAN_POS=$(( (RUNS + 1) / 2 )) TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") + TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1) + TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1) + TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }") + ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -) { echo "real_time_s=$TIME_MEDIAN" echo "real_peak_mb=$HEAP_MEDIAN" echo "real_epochs=$EPOCHS" + echo "real_runs=$RUNS" + echo "real_time_spread=$TIME_SPREAD" + echo "real_all_times=$ALL_TIMES" echo "real_input=$(basename "$REAL_INPUT")" } | tee -a /tmp/metrics.txt >> "$GITHUB_OUTPUT" @@ -663,28 +678,37 @@ jobs: # --- Real block (continuations, 1 sample) --- # Only reached when no cached baseline exists, which is the expensive case: - # push-to-main normally publishes the real-block numbers, so a /bench-real - # comment pays for the PR side alone. $REAL_INPUT was resolved before this + # push-to-main normally publishes the real-block numbers, so a /bench comment + # pays for the PR side alone. $REAL_INPUT was resolved before this # checkout and the fixture is untracked, so main proves the identical block. if [ "$RUN_REAL" != "true" ]; then - echo "Skipping real-block baseline (use /bench-real to enable)" + echo "Skipping real-block baseline (fixture URL unset)" else - echo "--- Baseline real block (continuations, epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" - ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \ - --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \ - -o /tmp/real_proof.bin --time | tee /tmp/baseline_real.txt - rm -f /tmp/real_proof.bin - T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_real.txt | awk '{print $3}') - # Optional, as on the PR side: a main that predates continuation heap - # reporting prints no such line, and that must not fail the comparison. - H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_real.txt | awk '{print $3}') - if [ -z "$T" ]; then - echo "::error::Failed to parse baseline real-block proving time" - cat /tmp/baseline_real.txt - exit 1 - fi - echo "real_time_s=$T" >> "$GITHUB_OUTPUT" - echo "real_peak_mb=$H" >> "$GITHUB_OUTPUT" + # Sampled to the same count as the PR side. A 3-vs-1 comparison would put + # the two sides' noise on different footings, which is precisely the error + # sampling exists to avoid. + RTIMES=""; RHEAPS="" + for i in $(seq 1 "$BENCH_RUNS_REAL"); do + echo "--- Baseline real block run $i/$BENCH_RUNS_REAL (epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" + ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \ + --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \ + -o /tmp/real_proof.bin --time | tee /tmp/baseline_real_$i.txt + rm -f /tmp/real_proof.bin + T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_real_$i.txt | awk '{print $3}') + # Optional, as on the PR side: a main that predates continuation heap + # reporting prints no such line, and that must not fail the comparison. + H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_real_$i.txt | awk '{print $3}') + if [ -z "$T" ]; then + echo "::error::Failed to parse baseline real-block proving time from run $i" + cat /tmp/baseline_real_$i.txt + exit 1 + fi + RTIMES="$RTIMES $T" + if [ -n "$H" ]; then RHEAPS="$RHEAPS $H"; fi + done + RMED_POS=$(( (BENCH_RUNS_REAL + 1) / 2 )) + echo "real_time_s=$(echo $RTIMES | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT" + echo "real_peak_mb=$(echo $RHEAPS | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT" fi # Restore PR checkout @@ -744,6 +768,9 @@ jobs: PR_REAL_PEAK: ${{ steps.pr-real.outputs.real_peak_mb }} PR_REAL_EPOCHS: ${{ steps.pr-real.outputs.real_epochs }} PR_REAL_INPUT: ${{ steps.pr-real.outputs.real_input }} + PR_REAL_RUNS: ${{ steps.pr-real.outputs.real_runs }} + PR_REAL_TIME_SPREAD: ${{ steps.pr-real.outputs.real_time_spread }} + PR_REAL_ALL_TIMES: ${{ steps.pr-real.outputs.real_all_times }} run: | # Pick baseline source if [ "$BASELINE_FOUND" = "true" ]; then @@ -829,6 +856,9 @@ jobs: echo "pr_real_peak=$PR_REAL_PEAK" >> "$GITHUB_OUTPUT" echo "pr_real_epochs=$PR_REAL_EPOCHS" >> "$GITHUB_OUTPUT" echo "pr_real_input=$PR_REAL_INPUT" >> "$GITHUB_OUTPUT" + echo "pr_real_runs=$PR_REAL_RUNS" >> "$GITHUB_OUTPUT" + echo "pr_real_time_spread=$PR_REAL_TIME_SPREAD" >> "$GITHUB_OUTPUT" + echo "pr_real_all_times=$PR_REAL_ALL_TIMES" >> "$GITHUB_OUTPUT" echo "baseline_real_time=$BASELINE_REAL_TIME" >> "$GITHUB_OUTPUT" echo "baseline_real_peak=$BASELINE_REAL_PEAK" >> "$GITHUB_OUTPUT" echo "baseline_real_input=$BASELINE_REAL_INPUT" >> "$GITHUB_OUTPUT" @@ -903,6 +933,9 @@ jobs: REAL_PEAK_PCT: ${{ steps.compare.outputs.real_peak_pct }} REAL_MISMATCH: ${{ steps.compare.outputs.real_mismatch }} REAL_EPOCH_LOG2: ${{ env.REAL_BLOCK_EPOCH_LOG2 }} + REAL_RUNS: ${{ steps.compare.outputs.pr_real_runs }} + REAL_TIME_SPREAD: ${{ steps.compare.outputs.pr_real_time_spread }} + REAL_ALL_TIMES: ${{ steps.compare.outputs.pr_real_all_times }} COMMIT_SHA: ${{ steps.pr-ref.outputs.sha || github.sha }} TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} with: @@ -955,18 +988,23 @@ jobs: const realPeakPct = process.env.REAL_PEAK_PCT; const realMismatch = process.env.REAL_MISMATCH; const realEpochLog2 = process.env.REAL_EPOCH_LOG2; + const realRuns = process.env.REAL_RUNS || '1'; + const realTimeSpread = process.env.REAL_TIME_SPREAD; + const realAllTimes = process.env.REAL_ALL_TIMES; // Stable marker: the "find and update the existing comment" lookup at the // bottom keys off it, so section headings can change without orphaning // every comment already posted. Legacy title matches stay as a fallback. let body = `\n`; - // --- Section 1: Real block (headline; only present on /bench-real, push, dispatch) --- + // --- Section 1: Real block (headline; present whenever the fixture is fetchable) --- if (realTime) { - body += `## Benchmark β€” real block${realInput ? ` (\`${realInput}\`)` : ''}\n\n`; + body += `## Benchmark β€” real block${realInput ? ` (\`${realInput}\`)` : ''}${parseInt(realRuns) > 1 ? ` (median of ${realRuns})` : ''}\n\n`; + // Run count lives in the heading ("median of N"), as on the synthetic + // section β€” repeating it here just made the two disagree in tone. body += `continuations Β· epoch 2^${realEpochLog2}`; if (realEpochs) body += ` Β· ${realEpochs} epochs`; - body += ` Β· 1 sample\n\n`; + body += `\n\n`; if (realMismatch) { body += `> ⚠️ Baseline measured a different block (\`${realMismatch}\`) β€” showing the PR side only.\n\n`; @@ -981,19 +1019,25 @@ jobs: } body += `| **Prove time** | ${baseRealTime}s | ${realTime}s | ${fmt(realTimeDiff)}s (${fmt(realTimePct)}%) ${icon(realTimePct)} |\n\n`; - // 1 sample, so the >5% band that gates the synthetic screen is not - // supportable here: call out only what is far outside run-to-run noise, - // and say plainly that the middle band is unresolved rather than "fine". + // Wider bands than the synthetic screen's 5%: 3 runs of a minutes-long + // prove resolve less than 5 runs of a 25s one, so the middle is reported + // as unresolved rather than as "fine". const rp = parseFloat(realTimePct); if (rp > 10) { body += `> ⚠️ **Regression on the real block** β€” prove time up ${fmt(realTimePct)}%.\n`; } else if (rp < -10) { body += `> πŸŽ‰ **Improvement on the real block** β€” prove time down ${realTimePct}%.\n`; } else if (Math.abs(rp) >= 3) { - body += `> ❓ **${fmt(realTimePct)}% β€” not resolved by one sample.** Re-run \`/bench-real\`, or use \`/bench-abba\` for a paired test.\n`; + body += `> ❓ **${fmt(realTimePct)}% β€” beyond what ${realRuns} runs resolve.** Use \`/bench-abba\` for a paired test.\n`; } else { body += `> βœ… No significant change.\n`; } + if (realTimeSpread && parseFloat(realTimeSpread) > 5.0) { + const vals = realAllTimes ? realAllTimes.split('/').map(t => `${t}s`).join(' / ') : ''; + body += `>\n> ⚠️ Real-block prove-time spread: ${realTimeSpread}% (${vals}) β€” the median above is less trustworthy than usual.\n`; + } else if (realTimeSpread && parseInt(realRuns) > 1) { + body += `>\n> Prove-time spread ${realTimeSpread}%${realAllTimes ? ` (${realAllTimes.split('/').map(t => `${t}s`).join(' / ')})` : ''}\n`; + } } else { body += `| Metric | PR |\n`; body += `|--------|----|\n`; @@ -1011,7 +1055,7 @@ jobs: const tableParallelism = process.env.TABLE_PARALLELISM; const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Synthetic screen β€” 20 plain transfers, ~6x less work than a real block and a very different keccak:ecrecover mix. Fast, so it runs on every \`/bench\`; comment \`/bench-real\` for the representative number. Table parallelism: ${tpLabel}\n\n`; + body += `Synthetic screen β€” 20 plain transfers, ~6x less work than the real block above and a very different keccak:ecrecover mix. Cheap enough to sample 5x, so it stays the high-sensitivity half of the pair. Table parallelism: ${tpLabel}\n\n`; body += `| Metric | main | PR | Ξ” |\n`; body += `|--------|------|----|---|\n`; body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; @@ -1135,9 +1179,13 @@ jobs: } // --- Footer --- + // The real block runs on every invocation, so its absence means the fixture + // could not be fetched β€” say that plainly rather than offering a command to + // re-request it, which no longer exists. if (!realTime) { - body += `\n> 🧱 **This ran on synthetic blocks only.** Comment \`/bench-real\` to prove a real Ethereum block `; - body += `(keccak/trie-dominated rather than ecrecover-dominated). It adds ~5 min to the run.\n`; + body += `\n> 🧱 **Synthetic blocks only β€” the real-block fixture was not available.** `; + body += `Check that \`ETHREX_REAL_BLOCK_FIXTURE_URL\` is set in the Makefile and that the artifact is reachable; `; + body += `the run log carries the warning.\n`; } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 62df16ded..064d4e011 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -15,7 +15,7 @@ # scripts/perf_diff.sh REF_A [REF_B=origin/main] # Env: WORKLOAD=synthetic|real (default synthetic) picks the block to profile; # EPOCH_SIZE_LOG2= (default 22) sizes the epoch, WORKLOAD=real only. -# 22 is the calibrated bench-runner tier, matching /bench-real; use 23 on a +# 22 is the calibrated bench-runner tier, matching /bench; use 23 on a # 128 GiB box (tooling/ethrex-real-block/README.md, "Choosing the epoch size"). # # Pick the workload that matches the run you are localizing, because the symbol @@ -48,7 +48,7 @@ REF_A="$1" REF_B="${2:-origin/main}" WORKLOAD="${WORKLOAD:-synthetic}" # 2^22: the calibrated tier for the bench server this script targets, same as -# /bench-real. Memory picks it, not speed β€” 32.2 GiB fits a >=64 GiB box with ~50% +# /bench's real-block arm. Memory picks it, not speed β€” 32.2 GiB fits a >=64 GiB box with ~50% # headroom where 2^23's 60 GiB does not. EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-22}" case "$WORKLOAD" in diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index b4125b1d9..44167669f 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -140,7 +140,7 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | Where | How to run it | Cost (current default) | |---|---|---| -| `benchmark-pr.yml` | `/bench-real` on a PR; automatic on push to main and `workflow_dispatch` | ~4–5 min (first run at epoch 2^22 confirms) | +| `benchmark-pr.yml` | **`/bench`** on a PR β€” no separate command; also push to main and `workflow_dispatch` | 3 runs, ~4–5 min each (first run at epoch 2^22 confirms) | | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~4.5–4.8 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~25 min | | `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | 59.87 s/prove on an RTX 5090 (see below) | @@ -206,10 +206,19 @@ and the other tiers. The bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64` to serialize. -Plain `/bench` deliberately stays on the synthetic 20-transfer block: it proves in -~25s against ~4.5–4.8 min, and one runner carries every `/bench`, `/bench-abba` and -`/bench-verify` in the repo. The synthetic number is a fast screen; the real block -is the number that means something. +**`/bench` runs both halves, one command.** The synthetic screen (5 runs, ~25s each) +and the real block (3 runs, ~4–5 min each) go out together; there is no separate +trigger for the real one. They answer different questions and neither replaces the +other β€” the synthetic block is cheap enough to sample hard, so it resolves smaller +deltas, while the real block is the one whose number means something. + +**The cost is a shared resource.** One runner carries every `/bench`, `/bench-abba` +and `/bench-verify` in the repo, and folding the real block in takes a `/bench` from +roughly a minute of proving to **~15–20 min**, on every comment and every push to +main. That was a deliberate trade β€” one command beating two β€” and `BENCH_RUNS_REAL` +in `benchmark-pr.yml` is the dial if it proves too expensive. Its per-run time on the +runner is still unmeasured (the calibration ran on a different box), so the first run +is what tells you whether 3 was the right number. Cycle counts here (8.73M synthetic, 50.78M real) are from merge `fdb92f67` (main @ `9ccdaf2`, clang 21). They move with guest optimisation and ~2% with the clang major, @@ -367,8 +376,8 @@ peak RSS is set by the epoch size rather than the trace length. **Do not read absolute seconds off this table for another machine.** The calibration box runs ~8.6 s/Mcycle against the bench runner's 5.31–5.62, so the *ratios* between epochs -transfer and the wall times do not. `/bench-real` now runs at 2^22 (it was 2^21); the -first run after that change establishes the runner's real number. +transfer and the wall times do not. `/bench`'s real-block arm now runs at 2^22 (it was +2^21); the first run after that change establishes the runner's real number. ### Verifying a repoint From 7a29bb13938e4b99f28fcc98069977f806a5ab9d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:16:01 -0300 Subject: [PATCH 21/27] bench(ci): /bench proves the real block only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the synthetic sampled screen from /bench. The job now proves one workload β€” 3 sampled runs of the real block on the PR side against the cached 3-run baseline push-to-main publishes β€” and the comment carries one table. Why the screen went, recorded in the workflow so the next person doesn't re-add it: 1. Its only unique coverage was the MONOLITHIC prove path, which is vestigial (reportedly slower than a single-epoch continuation). Runner time spent covering a path we intend to delete is not a trade worth making. 2. Its crypto mix is one no real block has β€” 8.83 ECSM/Mcycle against a real block's ~1.3-1.6 β€” so screening against it tunes the prover for a worst case that cannot occur. Net -280 lines: the synthetic prove loop, its baseline arm, its half of the metrics artifact, its comparison arithmetic, and its section of the renderer all go together. Leaving the baseline arm behind would have kept proving numbers nothing reads. `/bench N` now sets the real-block sample count, replacing the old PR-vs-baseline split (3 and 5). Both sides use one count deliberately: an asymmetric count puts the two sides' noise on different footings, which is the error sampling exists to prevent. Still clamped to 5 β€” at ~4-5 min a run that is also the difference between a 15 and a 25 minute occupancy of the one bench runner. Untouched, as scoped: GROWTH_PROGRAMS and the synthetic fixture generation it needs (/bench-growth still sweeps them for a heap-vs-block-size slope, which needs a family of blocks and cannot come from one real one), /bench-abba, /bench-verify. The block-identity guard still refuses to diff across a repoint. Two things the removal exposed and this fixes: - metrics.txt was created by the synthetic step with `>` and appended to by everything else. It is now truncated in the config step, which always runs β€” otherwise a persistent self-hosted runner would have carried a previous run's values forward. - A missing baseline is no longer an error. The old code hard-failed on a zero/empty synthetic baseline; for the real block an absent baseline is the normal state before main has published one, so it now emits a notice and renders the PR side alone. Renderer wording, caught by the scenario harness: "prove time down -17.2%" and "up +17.3%" double-signed themselves, and the no-fixture footer still said "synthetic blocks only" β€” which is now simply false, since nothing else is proven. Exercised over 9 scenarios (normal, improvement, regression, unresolved band, bad spread, no baseline, block mismatch, fixture unavailable, growth sweep). --- .github/workflows/benchmark-pr.yml | 388 ++++------------------------ tooling/ethrex-real-block/README.md | 36 ++- 2 files changed, 78 insertions(+), 346 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 55c5e674a..d2dcc39cb 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -39,23 +39,27 @@ concurrency: cancel-in-progress: false env: - # Cheap screen: the ethrex guest ELF proven against a SYNTHETIC 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). + # `/bench` proves ONE workload: a real Ethereum block. The synthetic N-transfer + # screen that used to run alongside it was removed deliberately β€” if you are about + # to add it back, these are the two reasons it went: # - # This is not a representative block β€” 20 plain transfers are ecrecover-heavy and - # touch almost no state: 8.73M cycles, 411 keccak calls, 80 ecsm calls. It stays - # the per-comment default only because it proves in ~25s; the representative - # number is the real block below, which runs on every invocation alongside it. + # 1. Its only unique coverage was the MONOLITHIC prove path, which is vestigial + # (reportedly slower than a 1-epoch continuation). Coverage of a path we intend + # to delete is not a reason to spend runner time. + # 2. Its crypto mix is one no real block has: 8.83 ECSM per Mcycle against a real + # block's ~1.3-1.6. Screening against it tunes the prover for a worst case that + # cannot occur. # - # Cycle counts here are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"), + # The synthetic fixtures themselves are NOT gone β€” /bench-growth still sweeps them to + # get a heap-vs-block-size slope, which needs a family of blocks and so cannot come + # from one real one. Only the headline screen was dropped. + # + # Cycle counts below are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"), # as of main @ 9ccdaf2 with clang 21. They move with guest optimisation (#861 gave the # guest thin LTO) and ~2% with the clang major on PATH, so a count quoted against a # different ELF reads as a regression β€” always pin the ELF when repeating one. ELF: executor/program_artifacts/rust/ethrex.elf - INPUT: executor/tests/ethrex_bench_20.bin - # Headline (representative) workload: a real Ethereum block. WHICH block lives in + # The workload: a real Ethereum block. WHICH block lives in # the Makefile and nowhere else β€” nothing in this file names one, so a repoint # moves this job without editing it. At the current default that is 50.78M cycles, # 10,478 keccak calls and 116 ecsm calls: ~5.8x the work of the synthetic block with a @@ -106,13 +110,9 @@ env: # runs land above ~6 min each, /bench becomes a ~25 min occupancy of a runner every # other bench queues behind, and this count is what to turn down (2, or 1) before # reaching for anything else. + # `/bench N` overrides this, clamped to [1,5]. Past 5 the cached comparison can't + # beat the ~1% session-drift wall anyway β€” that is what /bench-abba is for. BENCH_RUNS_REAL: 3 - BENCH_RUNS_PR: 3 - # Cheap-tier screen: catches regressions down to ~1.5% on its own and leaves - # smaller/ambiguous deltas to the manual drift-free ABBA tiebreaker. Pushing either - # side past 5 buys little here (the cached comparison can't beat the ~1% drift wall), - # so the per-PR run count is also capped at 5 (clamp below). - BENCH_RUNS_BASELINE: 5 # 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). @@ -237,20 +237,28 @@ jobs: # workload even when main's Makefile names a different block. echo "REAL_INPUT=$(make -s print-real-block-fixture)" >> "$GITHUB_ENV" + # Every metrics writer appends; the step that used to create this file was the + # synthetic screen, which is gone. Truncate here (this step always runs) so a + # previous run's values can't survive on the persistent self-hosted runner. + : > /tmp/metrics.txt + + # `/bench N` sets the real-block sample count. Both sides use the same count + # (push-to-main publishes the baseline), so there is no PR-vs-baseline split + # any more β€” an asymmetric count would put the two sides' noise on different + # footings, which is what sampling is for. + RUNS=$BENCH_RUNS_REAL if [ "$EVENT_NAME" = "issue_comment" ]; then CUSTOM_N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench[[:space:]]*\([0-9]\+\).*|\1|p') - RUNS=${CUSTOM_N:-$BENCH_RUNS_PR} - elif [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then - RUNS=$BENCH_RUNS_BASELINE - else - RUNS=$BENCH_RUNS_PR + RUNS=${CUSTOM_N:-$BENCH_RUNS_REAL} fi # Clamp to 1-5. Beyond 5 the single-session cached comparison barely improves # (it can't beat the ~1% drift wall); use the ABBA tiebreaker for finer deltas. + # At ~4-5 min a run this is also the difference between a 15 min and a 25 min + # occupancy of the one bench runner. if [ "$RUNS" -lt 1 ] 2>/dev/null || [ "$RUNS" -gt 5 ] 2>/dev/null; then - echo "::warning::Run count $RUNS out of range [1,5], defaulting to $BENCH_RUNS_PR" - RUNS=$BENCH_RUNS_PR + echo "::warning::Run count $RUNS out of range [1,5], defaulting to $BENCH_RUNS_REAL" + RUNS=$BENCH_RUNS_REAL fi echo "runs=$RUNS" >> "$GITHUB_OUTPUT" @@ -285,75 +293,6 @@ jobs: make ethrex-real-block-fixture ls -l "$REAL_INPUT" - - name: Benchmark PR - id: pr - env: - RUNS: ${{ steps.config.outputs.runs }} - TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} - run: | - if [ -n "$TABLE_PARALLELISM" ]; then - export TABLE_PARALLELISM - echo "TABLE_PARALLELISM=$TABLE_PARALLELISM" - fi - TIMES="" - HEAPS="" - for i in $(seq 1 $RUNS); do - echo "--- Run $i/$RUNS ---" - ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \ - | tee /tmp/cli_output_$i.txt - rm -f /tmp/proof.bin - - T=$(grep -o 'Proving time: [0-9.]*' /tmp/cli_output_$i.txt | awk '{print $3}') - H=$(grep -o 'Peak heap: [0-9]*' /tmp/cli_output_$i.txt | awk '{print $3}') - - if [ -z "$T" ] || [ -z "$H" ]; then - echo "::error::Failed to parse metrics from run $i" - cat /tmp/cli_output_$i.txt - exit 1 - fi - - TIMES="$TIMES $T" - HEAPS="$HEAPS $H" - done - - # Median position (works for odd N; for even N picks lower-middle) - MEDIAN_POS=$(( (RUNS + 1) / 2 )) - TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") - HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") - - if [ -z "$HEAP_MEDIAN" ] || [ -z "$TIME_MEDIAN" ]; then - echo "::error::Failed to compute median metrics" - exit 1 - fi - - # Spread: (max - min) / median * 100 - TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1) - TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1) - TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }") - - HEAP_MIN=$(echo $HEAPS | tr ' ' '\n' | sort -n | head -1) - HEAP_MAX=$(echo $HEAPS | tr ' ' '\n' | sort -n | tail -1) - HEAP_SPREAD=$(awk "BEGIN { if ($HEAP_MEDIAN > 0) printf \"%.1f\", (($HEAP_MAX - $HEAP_MIN) / $HEAP_MEDIAN) * 100; else print \"0.0\" }") - - ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -) - ALL_HEAPS=$(echo $HEAPS | tr ' ' '\n' | paste -sd '/' -) - - echo "peak_mb=$HEAP_MEDIAN" >> "$GITHUB_OUTPUT" - echo "time_s=$TIME_MEDIAN" >> "$GITHUB_OUTPUT" - echo "time_spread=$TIME_SPREAD" >> "$GITHUB_OUTPUT" - echo "heap_spread=$HEAP_SPREAD" >> "$GITHUB_OUTPUT" - echo "all_times=$ALL_TIMES" >> "$GITHUB_OUTPUT" - echo "all_heaps=$ALL_HEAPS" >> "$GITHUB_OUTPUT" - echo "runs=$RUNS" >> "$GITHUB_OUTPUT" - - echo "peak_mb=$HEAP_MEDIAN" > /tmp/metrics.txt - echo "time_s=$TIME_MEDIAN" >> /tmp/metrics.txt - echo "time_spread=$TIME_SPREAD" >> /tmp/metrics.txt - echo "heap_spread=$HEAP_SPREAD" >> /tmp/metrics.txt - echo "all_times=$ALL_TIMES" >> /tmp/metrics.txt - echo "all_heaps=$ALL_HEAPS" >> /tmp/metrics.txt - echo "runs=$RUNS" >> /tmp/metrics.txt - - name: Memory growth (PR) id: pr-growth if: steps.config.outputs.run_growth == 'true' @@ -432,7 +371,7 @@ jobs: id: pr-real if: steps.config.outputs.run_real == 'true' env: - RUNS: ${{ env.BENCH_RUNS_REAL }} + RUNS: ${{ steps.config.outputs.runs }} TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} run: | if [ -n "$TABLE_PARALLELISM" ]; then @@ -518,15 +457,13 @@ jobs: BASELINE_FILE=$(ls -t baseline/*/metrics.txt 2>/dev/null | head -1) if [ -n "$BASELINE_FILE" ]; then echo "found=true" >> "$GITHUB_OUTPUT" - # Anchored (`^key=`): the real-block keys added below are suffixes of - # the plain ones (`real_time_s` contains `time_s`), so an unanchored - # grep would return TWO lines and write a multi-line step output. + # Anchored (`^key=`): `real_time_s` contains `time_s`, so an unanchored + # grep could match two lines and write a multi-line step output. get() { grep "^$1=" "$BASELINE_FILE" | head -1 | cut -d= -f2; } - for key in peak_mb time_s time_spread heap_spread all_times all_heaps \ - runs growth_heaps growth_times growth_slope_mb growth_r2 \ + for key in growth_heaps growth_times growth_slope_mb growth_r2 \ real_time_s real_peak_mb real_input; do - # Real-block keys are absent from baselines produced before the - # real-block section existed; empty values just hide that table. + # A baseline predating the real block simply has no real_* keys; empty + # values hide the table rather than producing a bogus comparison. echo "$key=$(get "$key")" >> "$GITHUB_OUTPUT" done exit 0 @@ -561,56 +498,6 @@ jobs: cargo build --release -p cli --features jemalloc-stats - # --- Primary benchmark (ethrex 20 transfers) --- - TIMES="" - HEAPS="" - for i in $(seq 1 $RUNS); do - echo "--- Baseline run $i/$RUNS ---" - ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \ - | tee /tmp/baseline_output_$i.txt - rm -f /tmp/proof.bin - - T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_output_$i.txt | awk '{print $3}') - H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_output_$i.txt | awk '{print $3}') - - if [ -z "$T" ] || [ -z "$H" ]; then - echo "::error::Failed to parse baseline metrics from run $i" - cat /tmp/baseline_output_$i.txt - exit 1 - fi - - TIMES="$TIMES $T" - HEAPS="$HEAPS $H" - done - - MEDIAN_POS=$(( (RUNS + 1) / 2 )) - TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") - HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS") - - if [ -z "$HEAP_MEDIAN" ] || [ -z "$TIME_MEDIAN" ]; then - echo "::error::Failed to compute baseline median metrics" - exit 1 - fi - - TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1) - TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1) - TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }") - - HEAP_MIN=$(echo $HEAPS | tr ' ' '\n' | sort -n | head -1) - HEAP_MAX=$(echo $HEAPS | tr ' ' '\n' | sort -n | tail -1) - HEAP_SPREAD=$(awk "BEGIN { if ($HEAP_MEDIAN > 0) printf \"%.1f\", (($HEAP_MAX - $HEAP_MIN) / $HEAP_MEDIAN) * 100; else print \"0.0\" }") - - ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -) - ALL_HEAPS=$(echo $HEAPS | tr ' ' '\n' | paste -sd '/' -) - - echo "peak_mb=$HEAP_MEDIAN" >> "$GITHUB_OUTPUT" - echo "time_s=$TIME_MEDIAN" >> "$GITHUB_OUTPUT" - echo "time_spread=$TIME_SPREAD" >> "$GITHUB_OUTPUT" - echo "heap_spread=$HEAP_SPREAD" >> "$GITHUB_OUTPUT" - echo "all_times=$ALL_TIMES" >> "$GITHUB_OUTPUT" - echo "all_heaps=$ALL_HEAPS" >> "$GITHUB_OUTPUT" - echo "runs=$RUNS" >> "$GITHUB_OUTPUT" - # --- Growth benchmarks (default parallelism, 1 sample each) --- # Only run if /bench-growth, push, or workflow_dispatch if [ "$RUN_GROWTH" != "true" ]; then @@ -688,8 +575,8 @@ jobs: # the two sides' noise on different footings, which is precisely the error # sampling exists to avoid. RTIMES=""; RHEAPS="" - for i in $(seq 1 "$BENCH_RUNS_REAL"); do - echo "--- Baseline real block run $i/$BENCH_RUNS_REAL (epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" + for i in $(seq 1 "$RUNS"); do + echo "--- Baseline real block run $i/$RUNS (epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---" ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \ --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \ -o /tmp/real_proof.bin --time | tee /tmp/baseline_real_$i.txt @@ -706,7 +593,7 @@ jobs: RTIMES="$RTIMES $T" if [ -n "$H" ]; then RHEAPS="$RHEAPS $H"; fi done - RMED_POS=$(( (BENCH_RUNS_REAL + 1) / 2 )) + RMED_POS=$(( (RUNS + 1) / 2 )) echo "real_time_s=$(echo $RTIMES | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT" echo "real_peak_mb=$(echo $RHEAPS | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT" fi @@ -722,13 +609,6 @@ jobs: env: # Baseline artifact outputs BASELINE_FOUND: ${{ steps.baseline-artifact.outputs.found }} - BA_PEAK_MB: ${{ steps.baseline-artifact.outputs.peak_mb }} - BA_TIME_S: ${{ steps.baseline-artifact.outputs.time_s }} - BA_TIME_SPREAD: ${{ steps.baseline-artifact.outputs.time_spread }} - BA_HEAP_SPREAD: ${{ steps.baseline-artifact.outputs.heap_spread }} - BA_ALL_TIMES: ${{ steps.baseline-artifact.outputs.all_times }} - BA_ALL_HEAPS: ${{ steps.baseline-artifact.outputs.all_heaps }} - BA_RUNS: ${{ steps.baseline-artifact.outputs.runs }} BA_GROWTH_HEAPS: ${{ steps.baseline-artifact.outputs.growth_heaps }} BA_GROWTH_TIMES: ${{ steps.baseline-artifact.outputs.growth_times }} BA_GROWTH_SLOPE: ${{ steps.baseline-artifact.outputs.growth_slope_mb }} @@ -737,27 +617,12 @@ jobs: BA_REAL_PEAK: ${{ steps.baseline-artifact.outputs.real_peak_mb }} BA_REAL_INPUT: ${{ steps.baseline-artifact.outputs.real_input }} # Baseline run outputs - BR_PEAK_MB: ${{ steps.baseline-run.outputs.peak_mb }} - BR_TIME_S: ${{ steps.baseline-run.outputs.time_s }} - BR_TIME_SPREAD: ${{ steps.baseline-run.outputs.time_spread }} - BR_HEAP_SPREAD: ${{ steps.baseline-run.outputs.heap_spread }} - BR_ALL_TIMES: ${{ steps.baseline-run.outputs.all_times }} - BR_ALL_HEAPS: ${{ steps.baseline-run.outputs.all_heaps }} - BR_RUNS: ${{ steps.baseline-run.outputs.runs }} BR_GROWTH_HEAPS: ${{ steps.baseline-run.outputs.growth_heaps }} BR_GROWTH_TIMES: ${{ steps.baseline-run.outputs.growth_times }} BR_GROWTH_SLOPE: ${{ steps.baseline-run.outputs.growth_slope_mb }} BR_GROWTH_R2: ${{ steps.baseline-run.outputs.growth_r2 }} BR_REAL_TIME: ${{ steps.baseline-run.outputs.real_time_s }} BR_REAL_PEAK: ${{ steps.baseline-run.outputs.real_peak_mb }} - # PR outputs - CURRENT_PEAK: ${{ steps.pr.outputs.peak_mb }} - CURRENT_TIME: ${{ steps.pr.outputs.time_s }} - PR_TIME_SPREAD: ${{ steps.pr.outputs.time_spread }} - PR_HEAP_SPREAD: ${{ steps.pr.outputs.heap_spread }} - PR_ALL_TIMES: ${{ steps.pr.outputs.all_times }} - PR_ALL_HEAPS: ${{ steps.pr.outputs.all_heaps }} - PR_RUNS: ${{ steps.pr.outputs.runs }} # PR growth outputs PR_GROWTH_HEAPS: ${{ steps.pr-growth.outputs.growth_heaps }} PR_GROWTH_TIMES: ${{ steps.pr-growth.outputs.growth_times }} @@ -774,14 +639,7 @@ jobs: run: | # Pick baseline source if [ "$BASELINE_FOUND" = "true" ]; then - BASELINE_PEAK="$BA_PEAK_MB" - BASELINE_TIME="$BA_TIME_S" BASELINE_SRC="cached" - BASELINE_TIME_SPREAD="$BA_TIME_SPREAD" - BASELINE_HEAP_SPREAD="$BA_HEAP_SPREAD" - BASELINE_ALL_TIMES="$BA_ALL_TIMES" - BASELINE_ALL_HEAPS="$BA_ALL_HEAPS" - BASELINE_RUNS="$BA_RUNS" BASELINE_GROWTH_HEAPS="$BA_GROWTH_HEAPS" BASELINE_GROWTH_TIMES="$BA_GROWTH_TIMES" BASELINE_GROWTH_SLOPE="$BA_GROWTH_SLOPE" @@ -790,14 +648,7 @@ jobs: BASELINE_REAL_PEAK="$BA_REAL_PEAK" BASELINE_REAL_INPUT="$BA_REAL_INPUT" else - BASELINE_PEAK="$BR_PEAK_MB" - BASELINE_TIME="$BR_TIME_S" BASELINE_SRC="built from main" - BASELINE_TIME_SPREAD="$BR_TIME_SPREAD" - BASELINE_HEAP_SPREAD="$BR_HEAP_SPREAD" - BASELINE_ALL_TIMES="$BR_ALL_TIMES" - BASELINE_ALL_HEAPS="$BR_ALL_HEAPS" - BASELINE_RUNS="$BR_RUNS" BASELINE_GROWTH_HEAPS="$BR_GROWTH_HEAPS" BASELINE_GROWTH_TIMES="$BR_GROWTH_TIMES" BASELINE_GROWTH_SLOPE="$BR_GROWTH_SLOPE" @@ -809,44 +660,15 @@ jobs: BASELINE_REAL_INPUT="$PR_REAL_INPUT" fi - if [ -z "$BASELINE_PEAK" ] || [ "$BASELINE_PEAK" -eq 0 ] 2>/dev/null || - [ -z "$BASELINE_TIME" ]; then - echo "::error::Invalid baseline values: peak=$BASELINE_PEAK time=$BASELINE_TIME" - exit 1 - fi - - PEAK_DIFF=$((CURRENT_PEAK - BASELINE_PEAK)) - PEAK_PCT=$(awk "BEGIN { printf \"%.1f\", ($PEAK_DIFF * 100) / $BASELINE_PEAK }") - TIME_DIFF=$(awk "BEGIN { printf \"%.3f\", $CURRENT_TIME - $BASELINE_TIME }") - TIME_PCT=$(awk "BEGIN { printf \"%.1f\", (($CURRENT_TIME - $BASELINE_TIME) * 100) / $BASELINE_TIME }") - - echo "baseline_peak=$BASELINE_PEAK" >> "$GITHUB_OUTPUT" - echo "baseline_time=$BASELINE_TIME" >> "$GITHUB_OUTPUT" echo "baseline_src=$BASELINE_SRC" >> "$GITHUB_OUTPUT" - echo "peak_diff=$PEAK_DIFF" >> "$GITHUB_OUTPUT" - echo "peak_pct=$PEAK_PCT" >> "$GITHUB_OUTPUT" - echo "time_diff=$TIME_DIFF" >> "$GITHUB_OUTPUT" - echo "time_pct=$TIME_PCT" >> "$GITHUB_OUTPUT" - echo "pr_time_spread=$PR_TIME_SPREAD" >> "$GITHUB_OUTPUT" - echo "pr_heap_spread=$PR_HEAP_SPREAD" >> "$GITHUB_OUTPUT" - echo "pr_all_times=$PR_ALL_TIMES" >> "$GITHUB_OUTPUT" - echo "pr_all_heaps=$PR_ALL_HEAPS" >> "$GITHUB_OUTPUT" - echo "pr_runs=$PR_RUNS" >> "$GITHUB_OUTPUT" - echo "baseline_time_spread=$BASELINE_TIME_SPREAD" >> "$GITHUB_OUTPUT" - echo "baseline_heap_spread=$BASELINE_HEAP_SPREAD" >> "$GITHUB_OUTPUT" - echo "baseline_all_times=$BASELINE_ALL_TIMES" >> "$GITHUB_OUTPUT" - echo "baseline_all_heaps=$BASELINE_ALL_HEAPS" >> "$GITHUB_OUTPUT" - echo "baseline_runs=$BASELINE_RUNS" >> "$GITHUB_OUTPUT" - - # Growth comparison - echo "pr_growth_heaps=$PR_GROWTH_HEAPS" >> "$GITHUB_OUTPUT" - echo "pr_growth_times=$PR_GROWTH_TIMES" >> "$GITHUB_OUTPUT" - echo "pr_growth_slope=$PR_GROWTH_SLOPE" >> "$GITHUB_OUTPUT" - echo "pr_growth_r2=$PR_GROWTH_R2" >> "$GITHUB_OUTPUT" - echo "baseline_growth_heaps=$BASELINE_GROWTH_HEAPS" >> "$GITHUB_OUTPUT" - echo "baseline_growth_times=$BASELINE_GROWTH_TIMES" >> "$GITHUB_OUTPUT" - echo "baseline_growth_slope=$BASELINE_GROWTH_SLOPE" >> "$GITHUB_OUTPUT" - echo "baseline_growth_r2=$BASELINE_GROWTH_R2" >> "$GITHUB_OUTPUT" + + # A missing real-block baseline is NOT an error: it is what a PR sees before + # main has published one, and the comment renders the PR side alone. Only a + # missing PR-side number means the run failed to measure anything, and the + # real-block step already exits non-zero in that case. + if [ -z "$BASELINE_REAL_TIME" ]; then + echo "::notice::No real-block baseline available; reporting the PR side only." + fi # Real-block comparison. Rendered only when BOTH sides have a number AND # they are the same block: a baseline captured before the Makefile was @@ -893,24 +715,7 @@ jobs: if: github.event_name != 'push' && github.event_name != 'workflow_dispatch' uses: actions/github-script@v7 env: - PR_PEAK: ${{ steps.pr.outputs.peak_mb }} - PR_TIME: ${{ steps.pr.outputs.time_s }} - BASELINE_PEAK: ${{ steps.compare.outputs.baseline_peak }} - BASELINE_TIME: ${{ steps.compare.outputs.baseline_time }} BASELINE_SRC: ${{ steps.compare.outputs.baseline_src }} - PEAK_PCT: ${{ steps.compare.outputs.peak_pct }} - TIME_PCT: ${{ steps.compare.outputs.time_pct }} - PEAK_DIFF: ${{ steps.compare.outputs.peak_diff }} - TIME_DIFF: ${{ steps.compare.outputs.time_diff }} - PR_RUNS: ${{ steps.compare.outputs.pr_runs }} - PR_TIME_SPREAD: ${{ steps.compare.outputs.pr_time_spread }} - PR_HEAP_SPREAD: ${{ steps.compare.outputs.pr_heap_spread }} - PR_ALL_TIMES: ${{ steps.compare.outputs.pr_all_times }} - PR_ALL_HEAPS: ${{ steps.compare.outputs.pr_all_heaps }} - BASE_TIME_SPREAD: ${{ steps.compare.outputs.baseline_time_spread }} - BASE_HEAP_SPREAD: ${{ steps.compare.outputs.baseline_heap_spread }} - BASE_ALL_TIMES: ${{ steps.compare.outputs.baseline_all_times }} - BASE_ALL_HEAPS: ${{ steps.compare.outputs.baseline_all_heaps }} PR_GROWTH_HEAPS: ${{ steps.compare.outputs.pr_growth_heaps }} PR_GROWTH_TIMES: ${{ steps.compare.outputs.pr_growth_times }} PR_GROWTH_SLOPE: ${{ steps.compare.outputs.pr_growth_slope }} @@ -940,24 +745,7 @@ jobs: TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} with: script: | - const peak = process.env.PR_PEAK; - const time = process.env.PR_TIME; - const basePeak = process.env.BASELINE_PEAK; - const baseTime = process.env.BASELINE_TIME; const baseSrc = process.env.BASELINE_SRC; - const peakPct = process.env.PEAK_PCT; - const timePct = process.env.TIME_PCT; - const peakDiff = process.env.PEAK_DIFF; - const timeDiff = process.env.TIME_DIFF; - const runs = process.env.PR_RUNS || '1'; - const prTimeSpread = process.env.PR_TIME_SPREAD; - const prHeapSpread = process.env.PR_HEAP_SPREAD; - const prAllTimes = process.env.PR_ALL_TIMES; - const prAllHeaps = process.env.PR_ALL_HEAPS; - const baseTimeSpread = process.env.BASE_TIME_SPREAD; - const baseHeapSpread = process.env.BASE_HEAP_SPREAD; - const baseAllTimes = process.env.BASE_ALL_TIMES; - const baseAllHeaps = process.env.BASE_ALL_HEAPS; // Growth data const prGrowthHeaps = process.env.PR_GROWTH_HEAPS; @@ -973,7 +761,6 @@ jobs: const fmt = (v) => parseFloat(v) >= 0 ? `+${v}` : v; const icon = (pct) => parseFloat(pct) > 5 ? 'πŸ”΄' : parseFloat(pct) < -5 ? '🟒' : 'βšͺ'; - const SPREAD_THRESHOLD = 5.0; // Real-block data const realTime = process.env.PR_REAL_TIME; @@ -1024,9 +811,9 @@ jobs: // as unresolved rather than as "fine". const rp = parseFloat(realTimePct); if (rp > 10) { - body += `> ⚠️ **Regression on the real block** β€” prove time up ${fmt(realTimePct)}%.\n`; + body += `> ⚠️ **Regression on the real block** β€” prove time up ${Math.abs(rp).toFixed(1)}%.\n`; } else if (rp < -10) { - body += `> πŸŽ‰ **Improvement on the real block** β€” prove time down ${realTimePct}%.\n`; + body += `> πŸŽ‰ **Improvement on the real block** β€” prove time down ${Math.abs(rp).toFixed(1)}%.\n`; } else if (Math.abs(rp) >= 3) { body += `> ❓ **${fmt(realTimePct)}% β€” beyond what ${realRuns} runs resolve.** Use \`/bench-abba\` for a paired test.\n`; } else { @@ -1050,73 +837,6 @@ jobs: body += `\n`; } - // --- Section 2: Synthetic screen --- - const nLabel = parseInt(runs) > 1 ? ` (median of ${runs})` : ''; - const tableParallelism = process.env.TABLE_PARALLELISM; - const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)'; - body += `## Benchmark β€” ethrex 20 transfers${nLabel}\n\n`; - body += `Synthetic screen β€” 20 plain transfers, ~6x less work than the real block above and a very different keccak:ecrecover mix. Cheap enough to sample 5x, so it stays the high-sensitivity half of the pair. Table parallelism: ${tpLabel}\n\n`; - body += `| Metric | main | PR | Ξ” |\n`; - body += `|--------|------|----|---|\n`; - body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`; - body += `| **Prove time** | ${baseTime}s | ${time}s | ${fmt(timeDiff)}s (${fmt(timePct)}%) ${icon(timePct)} |\n\n`; - - const regression = parseFloat(peakPct) > 5 || parseFloat(timePct) > 5; - const improvement = parseFloat(peakPct) < -5 || parseFloat(timePct) < -5; - if (regression) { - body += `> ⚠️ **Regression detected** β€” heap or time increased by more than 5%.\n`; - } else if (improvement) { - body += `> πŸŽ‰ **Improvement detected** β€” heap or time decreased by more than 5%.\n`; - } else { - body += `> βœ… No significant change.\n`; - } - - // Tier-1 -> Tier-2 escalation: a small time speedup the cheap 3-5 run CI - // can't confirm (it catches >=~1.5% on its own). Point at the drift-free - // ABBA tiebreaker, which the user runs on demand via `/bench-abba`. - const tp = parseFloat(timePct); - if (tp < 0 && tp > -1.5) { - body += `>\n`; - body += `> πŸ”¬ **Looks like a small speedup (${fmt(timePct)}%) β€” below what ${runs} runs can confirm.** `; - body += `Comment \`/bench-abba\` to run the drift-free ABBA tiebreaker (paired-t CI + exact Wilcoxon). `; - body += `Note: it occupies the bench server for ~30–40 min.\n`; - body += `> Optional pair count: \`/bench-abba 32\` (20 resolves ~1%, 32 for ~0.6%).\n`; - } - - // Spread warnings - const prWarnings = []; - const baseWarnings = []; - - if (prTimeSpread && parseFloat(prTimeSpread) > SPREAD_THRESHOLD) { - const vals = prAllTimes ? prAllTimes.split('/').map(t => `${t}s`).join(' / ') : ''; - prWarnings.push(`Prove time spread: ${prTimeSpread}% (${vals})`); - } - if (prHeapSpread && parseFloat(prHeapSpread) > SPREAD_THRESHOLD) { - const vals = prAllHeaps ? prAllHeaps.split('/').map(h => `${h} MB`).join(' / ') : ''; - prWarnings.push(`Heap spread: ${prHeapSpread}% (${vals})`); - } - if (baseTimeSpread && parseFloat(baseTimeSpread) > SPREAD_THRESHOLD) { - const vals = baseAllTimes ? baseAllTimes.split('/').map(t => `${t}s`).join(' / ') : ''; - baseWarnings.push(`Baseline time spread: ${baseTimeSpread}% (${vals}) β€” comparison may be less reliable`); - } - if (baseHeapSpread && parseFloat(baseHeapSpread) > SPREAD_THRESHOLD) { - const vals = baseAllHeaps ? baseAllHeaps.split('/').map(h => `${h} MB`).join(' / ') : ''; - baseWarnings.push(`Baseline heap spread: ${baseHeapSpread}% (${vals}) β€” comparison may be less reliable`); - } - - const allWarnings = [...prWarnings, ...baseWarnings]; - if (allWarnings.length > 0) { - body += `\n`; - for (const w of allWarnings) { - body += `> ⚠️ ${w}\n`; - } - if (prWarnings.length > 0) { - body += `> Consider re-running \`/bench\`\n`; - } - } else if (parseInt(runs) > 1) { - body += `\n> βœ… Low variance (time: ${prTimeSpread || '0.0'}%, heap: ${prHeapSpread || '0.0'}%)\n`; - } - // --- Section 3: Memory growth --- // Synthetic on purpose: this plots heap against BLOCK SIZE, which needs a // family of blocks that differ only in transaction count. A real block is @@ -1183,9 +903,9 @@ jobs: // could not be fetched β€” say that plainly rather than offering a command to // re-request it, which no longer exists. if (!realTime) { - body += `\n> 🧱 **Synthetic blocks only β€” the real-block fixture was not available.** `; - body += `Check that \`ETHREX_REAL_BLOCK_FIXTURE_URL\` is set in the Makefile and that the artifact is reachable; `; - body += `the run log carries the warning.\n`; + body += `\n> 🧱 **No prover measurement β€” the real-block fixture was not available.** `; + body += `\`/bench\` proves only the real block, so nothing was measured this run. `; + body += `Check that \`ETHREX_REAL_BLOCK_FIXTURE_URL\` is set in the Makefile and that the artifact is reachable; the run log carries the warning.\n`; } const sha = process.env.COMMIT_SHA.substring(0, 8); body += `\nCommit: ${sha} Β· Baseline: ${baseSrc} Β· Runner: self-hosted bench\n`; diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-real-block/README.md index 44167669f..d1ad55023 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-real-block/README.md @@ -140,7 +140,7 @@ it β€” see [Measured cost of candidate blocks](#measured-cost-of-candidate-block | Where | How to run it | Cost (current default) | |---|---|---| -| `benchmark-pr.yml` | **`/bench`** on a PR β€” no separate command; also push to main and `workflow_dispatch` | 3 runs, ~4–5 min each (first run at epoch 2^22 confirms) | +| `benchmark-pr.yml` | **`/bench`** on a PR β€” the only workload it proves; also push to main and `workflow_dispatch` | 3 runs, ~4–5 min each (first run at epoch 2^22 confirms) | | `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh ` | ~4.5–4.8 min per side, then cached | | `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ` | 5 recordings, so ~25 min | | `benchmark-gpu.yml` | `/bench-gpu` on a PR β€” **the default there** | 59.87 s/prove on an RTX 5090 (see below) | @@ -206,19 +206,31 @@ and the other tiers. The bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64` to serialize. -**`/bench` runs both halves, one command.** The synthetic screen (5 runs, ~25s each) -and the real block (3 runs, ~4–5 min each) go out together; there is no separate -trigger for the real one. They answer different questions and neither replaces the -other β€” the synthetic block is cheap enough to sample hard, so it resolves smaller -deltas, while the real block is the one whose number means something. +**`/bench` proves this block and nothing else** β€” 3 sampled runs against the cached +3-run baseline main publishes on every push. `/bench N` changes the sample count +(clamped to 5). + +The synthetic N-transfer screen that used to run alongside it was **removed**. Two +reasons, recorded here because "add a cheap screen back" is an easy suggestion to +make twice: + +1. Its only unique coverage was the **monolithic** prove path, which is vestigial β€” + reportedly slower than a single-epoch continuation. Spending runner time to cover + a path we intend to delete is not a trade worth making. +2. Its crypto mix is one **no real block has**: 8.83 ECSM per Mcycle, against a real + block's ~1.3–1.6. Screening against it tunes the prover for a worst case that + cannot occur. + +The synthetic fixtures themselves are not gone β€” `/bench-growth` still sweeps them for +a heap-vs-block-size slope, which needs a family of blocks and so cannot come from one +real one. `/bench-abba` and `/bench-verify` are untouched. **The cost is a shared resource.** One runner carries every `/bench`, `/bench-abba` -and `/bench-verify` in the repo, and folding the real block in takes a `/bench` from -roughly a minute of proving to **~15–20 min**, on every comment and every push to -main. That was a deliberate trade β€” one command beating two β€” and `BENCH_RUNS_REAL` -in `benchmark-pr.yml` is the dial if it proves too expensive. Its per-run time on the -runner is still unmeasured (the calibration ran on a different box), so the first run -is what tells you whether 3 was the right number. +and `/bench-verify` in the repo, and a `/bench` now occupies it for **~15–20 min**, on +every comment and every push to main. `BENCH_RUNS_REAL` in `benchmark-pr.yml` is the +dial. Its per-run time on that runner is still unmeasured β€” the epoch calibration ran +on a different box β€” so the first run is what tells you whether 3 was the right +number; above ~6 min a run, turn it down. Cycle counts here (8.73M synthetic, 50.78M real) are from merge `fdb92f67` (main @ `9ccdaf2`, clang 21). They move with guest optimisation and ~2% with the clang major, From 6af5d689f5054320bb0dde37258127847f6f9869 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:18:47 -0300 Subject: [PATCH 22/27] test(bench): commit the PR-comment renderer harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit benchmark-pr.yml's comment step is ~200 lines of JS embedded in YAML, and the only other way to see its output was to push and run a real benchmark β€” now ~15-20 min of occupancy on the single shared bench runner. This extracts the script block and renders it against synthetic env values, so a wording or formatting change is checked in a second. It renders rather than asserts, deliberately: the failures it has actually caught were readability ones a human spots instantly and an assertion would have to anticipate β€” double-signed percentages ("prove time down -17.2%"), and a footer that still claimed "synthetic blocks only" after the synthetic screen was removed. The nine scenarios are the renderer's branches: normal, improvement, regression, the unresolved 3-10% band, bad spread, no baseline yet, block mismatch, fixture unavailable, and the growth sweep. It lived in a scratchpad while the workflow was in flux; committing it so the next person editing that comment has it. --- .github/workflows/benchmark-pr.yml | 4 ++ scripts/render_bench_comment.js | 99 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 scripts/render_bench_comment.js diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index d2dcc39cb..c21d6cdb2 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -744,6 +744,10 @@ jobs: COMMIT_SHA: ${{ steps.pr-ref.outputs.sha || github.sha }} TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }} with: + # Renderable offline: `node scripts/render_bench_comment.js` extracts this block + # and prints the markdown for a set of scenarios, so wording and formatting can + # be checked without occupying the bench server for 15-20 min. Add a scenario + # there when you add a branch here. script: | const baseSrc = process.env.BASELINE_SRC; diff --git a/scripts/render_bench_comment.js b/scripts/render_bench_comment.js new file mode 100644 index 000000000..eee39f231 --- /dev/null +++ b/scripts/render_bench_comment.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +// +// render_bench_comment.js β€” renders benchmark-pr.yml's PR-comment script offline. +// +// WHY: that comment is ~200 lines of JS embedded in YAML, and the only other way to +// see its output is to push and run a real benchmark on the shared bench server +// (~15-20 min of occupancy). This extracts the `script:` block from the workflow, +// runs it against synthetic env values, and prints the markdown β€” so a wording or +// formatting change is checked in a second. +// +// It renders; it does not assert. Read the output. The scenarios below are the ones +// that have actually broken before: sign handling on the verdict lines, the +// no-baseline and block-mismatch paths, and the fixture-unavailable footer. +// +// USAGE: node scripts/render_bench_comment.js +// +// Add a scenario by adding a key to `scenarios`; each is the env the comment step +// would see. Keep one per branch of the renderer. +const fs = require('fs'); +const path = require('path'); +const wfPath = path.join(__dirname, '..', '.github', 'workflows', 'benchmark-pr.yml'); +const wf = fs.readFileSync(wfPath, 'utf8'); +const lines = wf.split('\n'); +const start = lines.map(l => l.trim() === 'script: |').lastIndexOf(true); +if (start < 0) throw new Error('script block not found'); +const indent = lines[start].match(/^\s*/)[0].length + 2; +const body = []; +for (let i = start + 1; i < lines.length; i++) { + const l = lines[i]; + if (l.trim() === '') { body.push(''); continue; } + if (l.match(/^\s*/)[0].length < indent) break; + body.push(l.slice(indent)); +} +const src = body.join('\n'); + +const captured = []; +const github = { rest: { issues: { + listComments: async () => ({ data: [] }), + createComment: async (a) => captured.push(a.body), + updateComment: async (a) => captured.push(a.body), +}}}; +const context = { repo: { owner: 'o', repo: 'r' }, issue: { number: 1 } }; + +const REAL = { + COMMIT_SHA: 'c4e42b2900', REAL_EPOCH_LOG2: '22', BASELINE_SRC: 'cached', + PR_REAL_TIME: '281.400', PR_REAL_PEAK: '32980', PR_REAL_EPOCHS: '13', + PR_REAL_INPUT: 'ethrex_mainnet_25368371.bin', + REAL_RUNS: '3', REAL_TIME_SPREAD: '1.9', REAL_ALL_TIMES: '279.1/281.4/284.4', +}; +const scenarios = { + 'A: normal /bench β€” real block vs cached baseline': { + ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + }, + 'B: clear improvement': { + ...REAL, BASE_REAL_TIME: '340.000', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '-58.600', REAL_TIME_PCT: '-17.2', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + }, + 'C: clear regression': { + ...REAL, BASE_REAL_TIME: '240.000', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '+41.400', REAL_TIME_PCT: '17.3', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + }, + 'D: unresolved middle band (3-10%)': { + ...REAL, BASE_REAL_TIME: '300.000', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '-18.600', REAL_TIME_PCT: '-6.2', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + }, + 'E: bad spread': { + ...REAL, REAL_TIME_SPREAD: '11.4', REAL_ALL_TIMES: '265.0/281.4/297.1', + BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + }, + 'F: no baseline yet': { ...REAL, BASELINE_SRC: 'built from main' }, + 'G: baseline measured a different block': { + ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975', + REAL_MISMATCH: 'ethrex_hoodi_1265656.bin', + }, + 'H: fixture unavailable': { COMMIT_SHA: 'c4e42b2900', REAL_EPOCH_LOG2: '22', BASELINE_SRC: 'cached' }, + 'I: with growth sweep (push-style)': { + ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975', + REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0', + PR_GROWTH_HEAPS: '18756/26966/34748/43896/50431', + PR_GROWTH_TIMES: '10.685/13.434/18.854/21.094/24.737', + PR_GROWTH_SLOPE: '2007', PR_GROWTH_R2: '0.9981', + BASE_GROWTH_HEAPS: '18700/26900/34700/43800/50217', + BASE_GROWTH_SLOPE: '2000', BASE_GROWTH_R2: '0.9980', + GROWTH_SLOPE_DIFF: '7', GROWTH_SLOPE_PCT: '0.4', + }, +}; + +(async () => { + for (const [name, env] of Object.entries(scenarios)) { + captured.length = 0; + process.env = { ...env }; + const fn = new Function('github', 'context', 'require', `return (async () => { ${src} })()`); + await fn(github, context, require); + console.log(`\n${'='.repeat(72)}\n### ${name}\n${'='.repeat(72)}`); + console.log(captured[0]); + } +})(); From 2826055d22c3f3f7b378d68eaf07072b62ca1a2f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:40:12 -0300 Subject: [PATCH 23/27] build(tooling): pin k256 features directly instead of promoting the crypto dev-dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review suggestion (Oppen): declaring k256 with `expose-field` in [dependencies] unifies the feature set between `cargo run` and `cargo test` just as well as promoting lambda-vm-ethrex-crypto out of [dev-dependencies] β€” resolver 3 unions the direct declaration into both resolutions β€” while keeping the test-only crypto crate where it belongs and dropping it from the converter binary's dependency graph entirely. Verified: `cargo tree -e features{,no-dev} -i k256` identical (both list expose-field); `cargo tree --no-dev` no longer contains lambda-vm-ethrex-crypto; converter tests 3/3. The trade is a duplicated feature requirement that must track crypto/ethrex-crypto's k256 line; the verification commands in the comment are the guard. --- tooling/ethrex-real-block/Cargo.lock | 1 + tooling/ethrex-real-block/Cargo.toml | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tooling/ethrex-real-block/Cargo.lock b/tooling/ethrex-real-block/Cargo.lock index 647596e66..bd2ceeb65 100644 --- a/tooling/ethrex-real-block/Cargo.lock +++ b/tooling/ethrex-real-block/Cargo.lock @@ -1080,6 +1080,7 @@ dependencies = [ "ethrex-common", "ethrex-config", "ethrex-guest-program", + "k256", "lambda-vm-ethrex-crypto", "rkyv", "serde", diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-real-block/Cargo.toml index b56a5848c..15b51dd47 100644 --- a/tooling/ethrex-real-block/Cargo.toml +++ b/tooling/ethrex-real-block/Cargo.toml @@ -32,22 +32,24 @@ rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths both -# fall back to software, so this exercises the guest's *fallback* crypto semantics -# β€” not the accelerators themselves. -# -# Used only by the tests, but declared here rather than in `[dev-dependencies]` on -# purpose: it is the only thing that asks for k256's `expose-field`, and under -# resolver 3 a dev-dependency's features are not unified into builds that exclude -# dev-dependencies. As a dev-dep, `cargo run` (the `make ethrex-real-block-fixture` -# step) and the `cargo test` that follows it in CI resolve two different k256 -# feature sets, so k256 and every ethrex crate above it compile a second time in -# the same job. Confirm with: +# Not used by this crate's code directly β€” declared to pin k256's feature set. +# `lambda-vm-ethrex-crypto` (a dev-dependency) is otherwise the only thing asking +# for `expose-field`, and under resolver 3 dev-dependency features are not unified +# into builds that exclude dev-dependencies: `cargo run` (the +# `make ethrex-real-block-fixture` step) and the `cargo test` that follows it in +# CI would resolve two different k256 feature sets, recompiling k256 and every +# ethrex crate above it in the same job. Declaring the features here puts them in +# both resolutions. Keep in sync with crypto/ethrex-crypto's k256 line; verify: # cargo tree -e features,no-dev -i k256 # must list expose-field # cargo tree -e features -i k256 # must match the above -lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } +k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic", "expose-field"] } [dev-dependencies] +# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths both +# fall back to software, so this exercises the guest's *fallback* crypto semantics +# β€” not the accelerators themselves. (Its k256 feature needs are mirrored in +# [dependencies] above β€” see the comment there.) +lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } # No feature delta against the rest of the graph (sha2/std is already on), so this # one can stay a dev-dependency without forcing the rebuild described above. sha2 = "0.10" From 96079f035208df58711beebd69569c5c7a8fde70 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:43:57 -0300 Subject: [PATCH 24/27] ci(ethrex): name the fixture-test workflow as what it is "ethrex real block" read as a benchmark; it is validation for the fixture and its converter (tests, not measurements). The bench lives in benchmark-pr.yml. --- .github/workflows/ethrex-real-block.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ethrex-real-block.yml b/.github/workflows/ethrex-real-block.yml index 460025102..0c283c49d 100644 --- a/.github/workflows/ethrex-real-block.yml +++ b/.github/workflows/ethrex-real-block.yml @@ -1,4 +1,4 @@ -name: ethrex real block +name: ethrex real-block fixture tests # Validation for the real-block benchmark fixture and the converter that produces # it. Deliberately NOT part of the required PR gate (pr_main.yaml): the fixture is a From 05993eed74b14579804d1fe1efccca6d30f6ef94 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:50:57 -0300 Subject: [PATCH 25/27] refactor(tooling): rename ethrex-real-block to ethrex-block-converter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crate was named after the motivation that produced it ("get a real block to benchmark") rather than after what it does. What it does is convert an ethrex-replay cache JSON β€” of any Ethereum block β€” into the rkyv ProgramInput fixture the guest deserializes. Nothing in it is specific to the benchmark block: its own tests are pinned to Hoodi 1265656 precisely because the conversion is what is under test, not the workload. Name it for its function. tooling/ethrex-real-block -> tooling/ethrex-block-converter package ethrex-real-block -> ethrex-block-converter .github/workflows/ ethrex-real-block.yml -> ethrex-block-converter.yml (display name "ethrex real-block fixture tests" -> "ethrex block-converter tests") Deliberately unchanged, because they name the BLOCK and that is still correct: the Makefile's ETHREX_REAL_BLOCK* variables and every `*-real-block-*` target, the `test_ethrex_real_block_{native,vm}` tests in tooling/ethrex-tests, the fixture filenames, release URLs and sha256s. Only path values pointing into the moved directory moved. Also unchanged: the "lambda-vm-ethrex-real-block-usable" rust-cache key in the renamed workflow. It keys the block-usability job, whose workspace (tooling/ethrex-tests) did not move β€” renaming it would discard a valid cache of the heavy ethrex tree for nothing. The converter job's key and its `workspaces:` entry did both move; that entry is load-bearing, since the crate is a detached workspace the default `. -> target` would miss. No behavior change. The converter's 3 tests pass and the reproducibility digest is untouched, so the fixture bytes are identical. --- .github/workflows/benchmark-pr.yml | 6 ++-- ...l-block.yml => ethrex-block-converter.yml} | 14 ++++----- .github/workflows/pr_main.yaml | 6 ++-- Makefile | 18 +++++------ executor/.gitignore | 2 +- executor/tests/README.md | 2 +- scripts/bench_verify.sh | 2 +- scripts/perf_diff.sh | 2 +- .../.gitignore | 0 .../Cargo.lock | 30 +++++++++---------- .../Cargo.toml | 2 +- .../README.md | 8 ++--- .../src/main.rs | 6 ++-- tooling/ethrex-tests/tests/ethrex.rs | 4 +-- 14 files changed, 52 insertions(+), 50 deletions(-) rename .github/workflows/{ethrex-real-block.yml => ethrex-block-converter.yml} (93%) rename tooling/{ethrex-real-block => ethrex-block-converter}/.gitignore (100%) rename tooling/{ethrex-real-block => ethrex-block-converter}/Cargo.lock (99%) rename tooling/{ethrex-real-block => ethrex-block-converter}/Cargo.toml (99%) rename tooling/{ethrex-real-block => ethrex-block-converter}/README.md (99%) rename tooling/{ethrex-real-block => ethrex-block-converter}/src/main.rs (98%) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index c21d6cdb2..f9c83ea3b 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -17,7 +17,7 @@ on: # bytes, the Makefile decides which block it is. Without them a repointed # block would leave main's baseline stale until some prover file happened # to change β€” and the comparison guard would suppress the table until then. - - 'tooling/ethrex-real-block/**' + - 'tooling/ethrex-block-converter/**' - 'Makefile' # Uncomment to auto-run on PRs: # pull_request: @@ -77,14 +77,14 @@ env: # current default would need ~240 GB and a heavier candidate far more. # `--continuations` makes peak heap a function of the epoch size instead of the # trace length. Costs move with the block; the per-candidate table is in - # tooling/ethrex-real-block/README.md. + # tooling/ethrex-block-converter/README.md. # # Budget ~1.2 GB of disk for the bundle each run β€” hence the `rm -f` after every # prove. A heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64`; # a PR branch predating that fix fails at write time rather than mismeasuring. # # Epoch 2^22, from the CPU sweep on 2026-07-31 (124 GiB / 32-core box, real block, - # branch vintage; full table in tooling/ethrex-real-block/README.md): + # branch vintage; full table in tooling/ethrex-block-converter/README.md): # # 2^21 464.26 s 18.43 GiB RSS 26 epochs 1.72 GB proof # 2^22 397.88 s 32.21 GiB RSS 13 epochs 1.15 GB proof diff --git a/.github/workflows/ethrex-real-block.yml b/.github/workflows/ethrex-block-converter.yml similarity index 93% rename from .github/workflows/ethrex-real-block.yml rename to .github/workflows/ethrex-block-converter.yml index 0c283c49d..62c9cf22e 100644 --- a/.github/workflows/ethrex-real-block.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -1,4 +1,4 @@ -name: ethrex real-block fixture tests +name: ethrex block-converter tests # Validation for the real-block benchmark fixture and the converter that produces # it. Deliberately NOT part of the required PR gate (pr_main.yaml): the fixture is a @@ -15,14 +15,14 @@ on: pull_request: branches: ["**"] paths: - - 'tooling/ethrex-real-block/**' + - 'tooling/ethrex-block-converter/**' - 'tooling/ethrex-tests/**' - 'Makefile' - - '.github/workflows/ethrex-real-block.yml' + - '.github/workflows/ethrex-block-converter.yml' push: branches: ["main"] paths: - - 'tooling/ethrex-real-block/**' + - 'tooling/ethrex-block-converter/**' - 'tooling/ethrex-tests/**' - 'Makefile' @@ -35,7 +35,7 @@ concurrency: jobs: converter: - name: Real-block converter tests + name: Block converter tests runs-on: ubuntu-latest steps: - name: Checkout sources @@ -49,12 +49,12 @@ jobs: with: # Own key, not pr_main.yaml's "lambda-vm-test": a shared key across jobs # that build different crate sets causes recompilation on both sides. - shared-key: "lambda-vm-ethrex-real-block" + shared-key: "lambda-vm-ethrex-block-converter" cache-all-crates: "true" # Detached workspace (own Cargo.lock, own target dir), so the default # `. -> target` would miss it and rebuild the ethrex tree every run. workspaces: | - tooling/ethrex-real-block -> target + tooling/ethrex-block-converter -> target # Downloads the pinned ethrex-replay cache (the converter's test input) and # runs the crate's tests: host-side parity through the guest's own diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index 7510a9497..5d560a63a 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -61,10 +61,10 @@ jobs: # from scratch every run. `cache-all-crates` only covers the registry, not # compiled artifacts. # - # ethrex-real-block is deliberately NOT here: this job no longer builds it. + # ethrex-block-converter is deliberately NOT here: this job no longer builds it. # That workspace is the expensive one (~335 packages, including the blst, # c-kzg and secp256k1-sys C builds, malachite and ark-ff/asm) and it now - # builds only in ethrex-real-block.yml, which carries its own cache entry. + # builds only in ethrex-block-converter.yml, which carries its own cache entry. workspaces: | . -> target tooling/ethrex-tests -> target @@ -125,7 +125,7 @@ jobs: # for the real-block fixture. That fixture is a benchmark input: it has to be # right once, not on every PR, and fetching plus screening it here put a network # download and a ~335-package build in the required gate for an artifact with no - # bearing on correctness. Both moved to ethrex-real-block.yml, which runs when + # bearing on correctness. Both moved to ethrex-block-converter.yml, which runs when # the converter or the block pin actually changes. `no_kzg_backend_linked` still # runs here β€” it is a pure unit test, and it is the property the real-block # screen over there depends on. diff --git a/Makefile b/Makefile index 4b90a786f..a3471bdb4 100644 --- a/Makefile +++ b/Makefile @@ -302,7 +302,7 @@ test-rust: compile-programs-rust # names a block. Outside this file the repoint touches only REAL_BLOCK_FIXTURE in # tooling/ethrex-tests, which points the usability screen at the block actually # being proven. The converter's own pins do NOT move β€” see below. -# tooling/ethrex-real-block/README.md carries the procedure and each candidate's +# tooling/ethrex-block-converter/README.md carries the procedure and each candidate's # measured cost. ETHREX_REAL_BLOCK_NETWORK := mainnet ETHREX_REAL_BLOCK := 25368371 @@ -315,7 +315,7 @@ ETHREX_REAL_BLOCK_CACHE_SHA256 := 7aa88a5f7c5755b7575870f95e6c5c26186947f5e9e0d5 ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin -ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_REAL_BLOCK_ID).json +ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-block-converter/caches/cache_$(ETHREX_REAL_BLOCK_ID).json # $(call ensure_verified,url,sha256,dest,label,url-var-name) # @@ -350,7 +350,7 @@ define ensure_verified echo "$(4): $(5) is unset." >&2; \ echo " The $(ETHREX_REAL_BLOCK_ID) $(4) is fetched, not built. Set $(5) in the" >&2; \ echo " Makefile to wherever the artifact is hosted; see" >&2; \ - echo " tooling/ethrex-real-block/README.md for how to produce and host one." >&2; \ + echo " tooling/ethrex-block-converter/README.md for how to produce and host one." >&2; \ exit 1; \ fi; \ mkdir -p $(dir $(3)); \ @@ -405,14 +405,14 @@ print-real-block-fixture-url: # the converter's reproducibility digest drift under a fixed input. ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656 -ETHREX_CONVERTER_CACHE := tooling/ethrex-real-block/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json +ETHREX_CONVERTER_CACHE := tooling/ethrex-block-converter/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json # The cache filename is keyed on the block only, and its download rule has no other # prerequisite, so make would treat an already-present cache as up to date across an # `ETHREX_REPLAY_REV` bump and silently keep reading the old input. Depending on a # rev-stamped marker makes a re-pin discard the stale cache; without it the mismatch # only surfaces downstream as a `conversion_is_reproducible` digest failure, which # reads as "regenerate the fixture" and points at the wrong thing. -ETHREX_REPLAY_REV_STAMP := tooling/ethrex-real-block/caches/.replay-rev-$(ETHREX_REPLAY_REV) +ETHREX_REPLAY_REV_STAMP := tooling/ethrex-block-converter/caches/.replay-rev-$(ETHREX_REPLAY_REV) $(ETHREX_REPLAY_REV_STAMP): mkdir -p $(dir $@) @@ -429,22 +429,22 @@ ethrex-real-block-converter-cache: $(ETHREX_CONVERTER_CACHE) # Converter correctness: host-side parity through the guest's own Crypto impl, the # network-rejection guard, and the reproducibility digest. Runs on changes to the -# converter (see .github/workflows/ethrex-real-block.yml), not on every PR. +# converter (see .github/workflows/ethrex-block-converter.yml), not on every PR. test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE) - cd tooling/ethrex-real-block && cargo test --release + cd tooling/ethrex-block-converter && cargo test --release # Manual regeneration of the BENCHMARK fixture (not the converter's test block): # fetches that block's own cache and re-converts it, overwriting the fixture in # place so you can hash the result and upload it. That upload, plus SHA256/URL at # the top, is how the fixture is actually replaced. regen-real-block-fixture: ethrex-real-block-cache - cd tooling/ethrex-real-block && \ + cd tooling/ethrex-block-converter && \ cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) # ethrex host-reference tests live in the detached `tooling/ethrex-tests` # workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock). # Needs the real-block fixture, so it needs the fixture URL to be set; CI runs the -# `-offline` variant below in the PR gate and this one only in ethrex-real-block.yml. +# `-offline` variant below in the PR gate and this one only in ethrex-block-converter.yml. test-ethrex: compile-programs-rust ethrex-real-block-fixture cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm diff --git a/executor/.gitignore b/executor/.gitignore index d3448fb7a..1229bde5c 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -1,7 +1,7 @@ /target /program_artifacts/rust # Real-block fixtures (~1 MB): generated by `make ethrex-real-block-fixture` -# from an ethrex-replay cache, never committed. See tooling/ethrex-real-block. +# from an ethrex-replay cache, never committed. See tooling/ethrex-block-converter. # One pattern per network the converter accepts (mainnet/hoodi/sepolia β€” it # rejects anything else), so repointing ETHREX_REAL_BLOCK_NETWORK in the Makefile # cannot silently make a ~1 MB fixture committable. diff --git a/executor/tests/README.md b/executor/tests/README.md index 820963ea1..80c16d4eb 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -60,5 +60,5 @@ For a representative workload β€” real contract execution, real trie depth, real bytecode β€” `make ethrex-real-block-fixture` generates `ethrex_hoodi_1265656.bin` from an ethrex-replay cache. It is ~1 MB, so it is gitignored and generated on demand rather than committed, and its checksum is -pinned in `tooling/ethrex-real-block/README.md` rather than above (the +pinned in `tooling/ethrex-block-converter/README.md` rather than above (the checksum script only covers committed fixtures). diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index e6d48bfcf..068b4c864 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -34,7 +34,7 @@ # CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). For # WORKLOAD=real prefer the calibrated tier for the box you are on β€” 2^22 on the # bench runner or a 64 GiB machine, 2^23 on a 128 GiB one (see -# tooling/ethrex-real-block/README.md, "Choosing the epoch size"); the default +# tooling/ethrex-block-converter/README.md, "Choosing the epoch size"); the default # below is chosen for the SYNTHETIC arm. 20 matches # scripts/bench_abba.sh, so this arm proves the same bundle shape /bench already # proves on the same server β€” and 20 txs at 2^20 is strictly cheaper than the diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 064d4e011..a6c0380be 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -16,7 +16,7 @@ # Env: WORKLOAD=synthetic|real (default synthetic) picks the block to profile; # EPOCH_SIZE_LOG2= (default 22) sizes the epoch, WORKLOAD=real only. # 22 is the calibrated bench-runner tier, matching /bench; use 23 on a -# 128 GiB box (tooling/ethrex-real-block/README.md, "Choosing the epoch size"). +# 128 GiB box (tooling/ethrex-block-converter/README.md, "Choosing the epoch size"). # # Pick the workload that matches the run you are localizing, because the symbol # mix follows the block: the synthetic default is 20 plain transfers (8.73M cycles, diff --git a/tooling/ethrex-real-block/.gitignore b/tooling/ethrex-block-converter/.gitignore similarity index 100% rename from tooling/ethrex-real-block/.gitignore rename to tooling/ethrex-block-converter/.gitignore diff --git a/tooling/ethrex-real-block/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock similarity index 99% rename from tooling/ethrex-real-block/Cargo.lock rename to tooling/ethrex-block-converter/Cargo.lock index bd2ceeb65..a8268a857 100644 --- a/tooling/ethrex-real-block/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -877,6 +877,21 @@ dependencies = [ "uint", ] +[[package]] +name = "ethrex-block-converter" +version = "0.1.0" +dependencies = [ + "ethrex-common", + "ethrex-config", + "ethrex-guest-program", + "k256", + "lambda-vm-ethrex-crypto", + "rkyv", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "ethrex-blockchain" version = "13.0.0" @@ -1073,21 +1088,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "ethrex-real-block" -version = "0.1.0" -dependencies = [ - "ethrex-common", - "ethrex-config", - "ethrex-guest-program", - "k256", - "lambda-vm-ethrex-crypto", - "rkyv", - "serde", - "serde_json", - "sha2", -] - [[package]] name = "ethrex-rlp" version = "13.0.0" diff --git a/tooling/ethrex-real-block/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml similarity index 99% rename from tooling/ethrex-real-block/Cargo.toml rename to tooling/ethrex-block-converter/Cargo.toml index 15b51dd47..b1796fa60 100644 --- a/tooling/ethrex-real-block/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ethrex-real-block" +name = "ethrex-block-converter" version = "0.1.0" edition = "2024" diff --git a/tooling/ethrex-real-block/README.md b/tooling/ethrex-block-converter/README.md similarity index 99% rename from tooling/ethrex-real-block/README.md rename to tooling/ethrex-block-converter/README.md index d1ad55023..1c5012ba8 100644 --- a/tooling/ethrex-real-block/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -1,4 +1,4 @@ -# ethrex-real-block +# ethrex-block-converter Converts a **real Ethereum block** into a serialized `ProgramInput` `.bin` for the lambda-vm ethrex guest, reading an [`ethrex-replay`][replay] cache JSON. @@ -116,7 +116,7 @@ Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_SHA256` and its URL Directly, against any cache file: ```bash -cd tooling/ethrex-real-block +cd tooling/ethrex-block-converter cargo run --release -- ``` @@ -242,7 +242,7 @@ someone measures. The checks themselves are described under [Validation](#validation) below; this is where each one executes. -`.github/workflows/ethrex-real-block.yml` runs them on changes to this crate, +`.github/workflows/ethrex-block-converter.yml` runs them on changes to this crate, `tooling/ethrex-tests`, or the `Makefile` β€” **not** on every PR. The fixture is a benchmark input, read by no product code; it has to be right when it changes, not on every commit, and running it per-PR put a network fetch and a cold build of ~335 @@ -441,7 +441,7 @@ What costs time is a cold build of the ethrex host dependency tree β€” ~335 packages, including the `blst`, `c-kzg` and `secp256k1-sys` C builds, `malachite` and `ark-ff/asm` β€” not the tests. That build is why these checks live in their own path-filtered workflow rather than the PR gate (see [Where validation -runs](#where-validation-runs)); `ethrex-real-block.yml` caches this workspace's +runs](#where-validation-runs)); `ethrex-block-converter.yml` caches this workspace's `target/` under its own key, so only cold runs pay it. These run against this crate's own pinned block (Hoodi 1265656), **not** the diff --git a/tooling/ethrex-real-block/src/main.rs b/tooling/ethrex-block-converter/src/main.rs similarity index 98% rename from tooling/ethrex-real-block/src/main.rs rename to tooling/ethrex-block-converter/src/main.rs index 4949242a4..f96b03f24 100644 --- a/tooling/ethrex-real-block/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -85,7 +85,9 @@ fn usage_and_exit(program: &str) -> ! { fn main() -> Result<(), Box> { let mut args = std::env::args(); - let program = args.next().unwrap_or_else(|| "ethrex-real-block".into()); + let program = args + .next() + .unwrap_or_else(|| "ethrex-block-converter".into()); let (Some(cache_path), Some(out_path)) = (args.next(), args.next()) else { usage_and_exit(&program); }; @@ -121,7 +123,7 @@ mod tests { /// block the benchmarks currently prove: it is the one cache ethrex-replay hosts /// upstream, so keeping the converter's test input there costs us no hosting and /// cannot drift. What is under test here is the CONVERSION, not the benchmark - /// workload β€” see tooling/ethrex-real-block/README.md. + /// workload β€” see tooling/ethrex-block-converter/README.md. /// /// `caches/` is gitignored and fetched on demand, so every test here fails on /// a clean checkout until the cache is downloaded. Say so instead of surfacing diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index d7026dca6..eaa811bf4 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -90,7 +90,7 @@ fn test_ethrex_empty_block() { const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin"; /// Host-only acceptance gate for the real-block fixture produced by -/// `tooling/ethrex-real-block` (`make ethrex-real-block-fixture`): the block is +/// `tooling/ethrex-block-converter` (`make ethrex-real-block-fixture`): the block is /// re-executed statelessly against its own witness, so a successful run means /// the recovered tries, codes and headers reproduce the header's post-state /// root. Needs no guest ELF, which is what keeps it runnable where the RV64 @@ -104,7 +104,7 @@ const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin"; /// dependency graph links no KZG backend, so a block calling point evaluation /// (0x0a) diverges from consensus here and fails. That property is incidental to /// the dep graph rather than declared, so `no_kzg_backend_linked` below pins it. -/// (`tooling/ethrex-real-block`'s parity test does NOT cover 0x0a β€” it links +/// (`tooling/ethrex-block-converter`'s parity test does NOT cover 0x0a β€” it links /// `c-kzg` transitively via `ethrex-config`.) #[test] fn test_ethrex_real_block_native() { From 3d110349031b0e98b4ee978f1d440f6000a75d1f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 19:52:34 -0300 Subject: [PATCH 26/27] ci(ethrex): drop the old crate name from the usability screen's cache key One-time cold cache on the next run; the key now names the screen rather than the renamed crate. --- .github/workflows/ethrex-block-converter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index 62c9cf22e..53c640e9b 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -78,7 +78,7 @@ jobs: - name: Cache cargo build artifacts uses: Swatinem/rust-cache@v2 with: - shared-key: "lambda-vm-ethrex-real-block-usable" + shared-key: "lambda-vm-real-block-usable" cache-all-crates: "true" workspaces: | tooling/ethrex-tests -> target From ac73dda860220ed62deeec77dc9566296a57a35d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 31 Jul 2026 20:06:43 -0300 Subject: [PATCH 27/27] bench(gpu): /bench-gpu proves the real block, only The synthetic cont[TX]/mono[TX] modes are gone: the workflow takes an optional pair count and nothing else, pins bench_abba.sh's knobs explicitly (the script is shared with the CPU ABBA flow), and drops the mode input, the token parsing, the per-mode epoch conditional and the rkyv pre-flight that existed only for large synthetic continuation proofs. The running-comment ETA now scales from the measured 4-pair run instead of quoting a synthetic figure, and the Vast RAM-floor comment cites the measured real-block peak. --- .github/workflows/benchmark-gpu.yml | 126 +++++------------------ tooling/ethrex-block-converter/README.md | 11 +- 2 files changed, 31 insertions(+), 106 deletions(-) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index cb8768a23..11fcbf222 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -6,16 +6,11 @@ name: Benchmark GPU (PR) # It builds the cli at the PR head and at main, runs N interleaved pairs on the GPU, # posts the paired-t + Wilcoxon verdict back to the PR, then ALWAYS destroys the box. # -# Triggered by a "/bench-gpu [N] [real|cont[TX]|mono[TX]]" comment on a PR (N = pair -# count, default 14) or via workflow_dispatch. +# Triggered by a "/bench-gpu [N]" comment on a PR (N = pair count, default 14) or via +# workflow_dispatch. # -# Workload default: the REAL BLOCK (--continuations). Unlike the CPU per-PR path, where -# the real block is opt-in because one shared runner serializes every bench in the repo, -# this workflow rents its own box per run β€” the cost is money, not queue time β€” so the -# representative workload is the default and the synthetic ones stay reachable for -# continuity: "cont[TX]" / "mono[TX]" select the N-transfer synthetic fixtures as before. -# Cont proofs need rkyv pointer_width_64 on both sides, so PR branches older than that -# fix must rebase before a cont bench. +# Workload: the real block (see tooling/ethrex-block-converter/README.md), proven with +# --continuations at the calibrated epoch size below. # Orchestration runs on a GitHub-hosted runner; all GPU work happens on the rented # Vast box (provisioned by the template onstart). # @@ -29,9 +24,6 @@ on: pairs: description: "Number of A/B/B/A pairs" default: "14" - mode: - description: "Workload: real (default, real block + --continuations), cont[TX] (synthetic N-transfer + --continuations, TX defaults to 100), or mono[TX] (synthetic, monolithic, TX defaults to 5)" - default: "real" issue_comment: types: [created] @@ -88,8 +80,8 @@ jobs: github.event.issue.pull_request && startsWith(github.event.comment.body, '/bench-gpu') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)) - # Provisioning + dual cuda build (~30 min) + 2*pairs proves (~3.5 min each at - # the default cont100). Sized for the 32-pair worst case (~4.5 hr) with headroom; + # Provisioning + dual cuda build + 2*pairs real-block proves (~2 min each, + # host-CPU-dependent). Sized for the 32-pair worst case with generous headroom; # teardown still always destroys the box. timeout-minutes: 330 steps: @@ -101,50 +93,30 @@ jobs: COMMENT_BODY: ${{ github.event.comment.body }} PR_NUM: ${{ github.event.issue.number }} DISPATCH_PAIRS: ${{ github.event.inputs.pairs }} - DISPATCH_MODE: ${{ github.event.inputs.mode }} DISPATCH_REF: ${{ github.ref_name }} run: | if [ "$EVENT_NAME" = "issue_comment" ]; then # Pin the head SHA (works for fork PRs; avoids a force-push race mid-run). HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid) OUT_PR_NUM="$PR_NUM"; OUT_HEAD_SHA="$HEAD_SHA"; OUT_BRANCH="" - # Everything after "/bench-gpu" on its line, tokens in any order: - # a number = pair count; cont[TX]/mono[TX] = workload (see loop below). + # Everything after "/bench-gpu" on its line: the only token is an + # optional pair count. ARGS=$(printf '%s' "$COMMENT_BODY" | tr -d '\r' | sed -n 's|^/bench-gpu||p' | head -n1) PAIRS=14 else # workflow_dispatch: compare this branch vs main. OUT_PR_NUM=""; OUT_HEAD_SHA=""; OUT_BRANCH="$DISPATCH_REF" - ARGS="$DISPATCH_MODE" + ARGS="" PAIRS=${DISPATCH_PAIRS:-14} fi - # Default: the real block. Any cont/mono token switches to the synthetic - # fixtures at that transfer count, which is how pre-existing numbers stay - # reproducible with the exact syntax that produced them. - BENCH_WORKLOAD=real; CONTINUATIONS=1; TX_COUNT=100 set -f # tokens must not glob-expand against the runner's CWD for tok in $ARGS; do case "$tok" in - real) BENCH_WORKLOAD=real; CONTINUATIONS=1 ;; - cont) BENCH_WORKLOAD=synthetic; CONTINUATIONS=1; TX_COUNT=100 ;; - mono) BENCH_WORKLOAD=synthetic; CONTINUATIONS=0; TX_COUNT=5 ;; - cont[0-9]*) BENCH_WORKLOAD=synthetic; CONTINUATIONS=1; TX_COUNT="${tok#cont}" ;; - mono[0-9]*) BENCH_WORKLOAD=synthetic; CONTINUATIONS=0; TX_COUNT="${tok#mono}" ;; [0-9]*) PAIRS="$tok" ;; *) echo "::warning::ignoring unrecognized token '$tok'" ;; esac done - # TX_COUNT and PAIRS are interpolated into the remote bash -lc below: enforce - # digits-only. Mono is capped at 5tx (monolithic peak heap grows with the - # trace; 20tx needs ~78 GB, over the 48 GB box floor). - if [ "$CONTINUATIONS" = "1" ]; then TX_DEFAULT=100; TX_MAX=100; else TX_DEFAULT=5; TX_MAX=5; fi - case "$TX_COUNT" in - ''|*[!0-9]*) echo "::warning::invalid tx count '$TX_COUNT'; using $TX_DEFAULT"; TX_COUNT=$TX_DEFAULT ;; - esac - if [ "$TX_COUNT" -lt 1 ] || [ "$TX_COUNT" -gt "$TX_MAX" ]; then - echo "::warning::tx count $TX_COUNT out of range [1,$TX_MAX] for this mode; using $TX_DEFAULT" - TX_COUNT=$TX_DEFAULT - fi + # PAIRS is interpolated into the remote bash -lc below: enforce digits-only. case "$PAIRS" in ''|*[!0-9]*) echo "::warning::invalid pair count '$PAIRS'; using 14"; PAIRS=14 ;; esac @@ -160,42 +132,14 @@ jobs: PAIRS=$((PAIRS + 1)) echo "::notice::rounded odd pair count up to $PAIRS so AB/BA orders balance" fi - if [ "$BENCH_WORKLOAD" = "real" ]; then - WORKLOAD="ethrex real block, continuations" - elif [ "$CONTINUATIONS" = "1" ]; then - WORKLOAD="ethrex ${TX_COUNT}tx synthetic continuations" - else - WORKLOAD="ethrex ${TX_COUNT}tx synthetic monolithic" - fi - # Outputs land before the fail-fast below so the always() result - # comment is fully labeled even when this step exits early. + WORKLOAD="ethrex real block, continuations" { echo "pr_num=$OUT_PR_NUM" echo "head_sha=$OUT_HEAD_SHA" echo "branch=$OUT_BRANCH" echo "pairs=$PAIRS" - echo "continuations=$CONTINUATIONS" - echo "tx_count=$TX_COUNT" - echo "bench_workload=$BENCH_WORKLOAD" echo "workload=$WORKLOAD" } >> "$GITHUB_OUTPUT" - # Fail fast if the PR side predates the pointer_width_64 fix: a >=40tx - # continuation proof exceeds rkyv's old 2 GiB cap and only dies after the - # full dual build (~1 hr of GPU rental). Skip the check when the fetch - # fails: gh api prints HTTP error bodies to stdout, so gate on its exit - # status AND on the payload looking like the manifest (it declares - # rkyv) β€” never treat an error blob as a missing feature. Purely - # textual; deletable once every open branch postdates the fix. - if [ "$BENCH_WORKLOAD" = "synthetic" ] && [ "$CONTINUATIONS" = "1" ] && [ "$TX_COUNT" -ge 40 ]; then - REF="${OUT_HEAD_SHA:-$OUT_BRANCH}" - if SIDE=$(gh api "repos/$GITHUB_REPOSITORY/contents/prover/Cargo.toml?ref=$REF" \ - -H "Accept: application/vnd.github.raw" 2>/dev/null) \ - && printf '%s' "$SIDE" | grep -q '^rkyv' \ - && ! printf '%s' "$SIDE" | grep -q pointer_width_64; then - echo "::error::PR branch predates the rkyv pointer_width_64 fix β€” a ${TX_COUNT}tx continuation proof cannot serialize. Rebase onto main, or bench with cont10/mono." - exit 1 - fi - fi echo "Using $PAIRS A/B/B/A pairs on $WORKLOAD" - name: Acknowledge (react + occupancy notice) @@ -204,7 +148,6 @@ jobs: env: PAIRS: ${{ steps.config.outputs.pairs }} WORKLOAD: ${{ steps.config.outputs.workload }} - BENCH_WORKLOAD: ${{ steps.config.outputs.bench_workload }} with: script: | await github.rest.reactions.createForIssueComment({ @@ -214,14 +157,12 @@ jobs: // Post the "started" notice under the SAME marker the result step uses, so the // result updates this comment in place (and re-runs reuse it rather than stacking). const marker = 'GPU Benchmark (ABBA)'; - // No GPU duration is quoted for the real block: we have never measured one, - // and the CPU rate does not transfer. The prior from the RTX 5090 sweep is that - // above epoch 2^21 the prover was CPU-bound at the serial producer, so real-block - // GPU time may land closer to CPU time than people expect. The first run sets it. - const eta = process.env.BENCH_WORKLOAD === 'real' - ? 'Runtime for the real block is UNMEASURED β€” this run establishes the baseline.' - : 'This takes ~2.5 hr at the synthetic 100tx workload.'; - const body = `## GPU Benchmark (ABBA) β€” running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. ${eta} The result will replace this comment.`; + // Reference: 4 pairs measured ~20 min end-to-end (rental + dual build + + // 8 proves). Per-prove wall varies with the rented host's CPU (the prover + // is partly host-CPU-bound), so scale expectations from pair count, not + // from a fixed per-run figure. + const mins = 12 + Number(process.env.PAIRS) * 2; + const body = `## GPU Benchmark (ABBA) β€” running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. Rough ETA ~${mins} min. The result will replace this comment.`; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, per_page: 100, @@ -273,14 +214,11 @@ jobs: # because vast can't numerically compare the driver_version string server-side. MIN_DRIVER: "580" run: | - # cpu_ram filter is in GB. Floor 48 GB, unchanged by the move to the real - # block: continuation peak heap is set by the EPOCH SIZE, not the block, so - # the real block costs the same ~10 GB working set at bench_abba.sh's default - # epoch 2^20, plus accumulated epoch proofs (~70 MB each, ~64 epochs) for a - # ~14 GB total β€” and ~18 GB if the epoch is ever raised to 2^21. Both sit far - # under this floor, which was already sized for the legacy 5tx monolithic - # prove. (The 20-transfer monolithic prove at ~78 GB heap set the previous - # 96 GB floor; nothing here goes monolithic on a real block.) + # cpu_ram filter is in GB. Floor 48 GB: the real block at epoch 2^22 peaks at + # ~36 GB host RSS on the CUDA path (measured, main vintage) β€” ~25% headroom. + # Continuation peak is set by the epoch size, not the block, so bigger blocks + # don't move it; raising the epoch would (see the calibration tables in + # tooling/ethrex-block-converter/README.md). # gpu_frac=1 requires a WHOLE-MACHINE offer (you rent every GPU on the host), so # Vast places no other tenant on the box: CPU cores, RAM/memory bandwidth, PCIe, # and NVMe are fully dedicated. Without it the "most expensive" sort below lands on @@ -408,14 +346,6 @@ jobs: run: | SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST" - # The calibrated epoch applies to the real block only. The synthetic cont[TX] - # tokens keep bench_abba.sh's 2^20 so a number recorded before this change still - # reproduces with the syntax that produced it β€” that reproducibility is the whole - # reason those tokens were kept. mono[TX] has no epoch at all. - EPOCH_ENV="" - if [ "$BENCH_WORKLOAD" = "real" ]; then - EPOCH_ENV="EPOCH_SIZE_LOG2=$GPU_REAL_EPOCH_LOG2" - fi echo "Waiting for the template onstart script to finish (Rust + LLVM + sysroot + clone)..." # The bootstrap's final stdout line is "=== done ===". Vast captures onstart # output to /var/log/onstart.log; fall back to checking the artifacts it leaves. @@ -447,12 +377,6 @@ jobs: HEAD_SHA: ${{ steps.config.outputs.head_sha }} BRANCH: ${{ steps.config.outputs.branch }} PAIRS: ${{ steps.config.outputs.pairs }} - CONTINUATIONS: ${{ steps.config.outputs.continuations }} - TX_COUNT: ${{ steps.config.outputs.tx_count }} - # Only ever the literal "real" or "synthetic" β€” the config step's case arms - # cannot emit anything else β€” so it is safe to interpolate into the remote - # `bash -lc` below, the same discipline as the digits-only TX_COUNT/PAIRS. - BENCH_WORKLOAD: ${{ steps.config.outputs.bench_workload }} run: | SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST" @@ -477,7 +401,9 @@ jobs: # explicit and robust to the template default changing.) The harness still builds the # cli at REF_A (the PR) and origin/main in isolated worktrees, runs PAIRS interleaved # A/B/B/A proves, and prints the paired-t CI + Wilcoxon verdict. BENCH_FEATURES routes - # the build through the CUDA prover path; CONTINUATIONS/TX_COUNT pick the workload. + # the build through the CUDA prover path. bench_abba.sh is shared with the CPU ABBA + # flow and has its own defaults, so its knobs are pinned explicitly here: the real + # block, continuations, the calibrated epoch. # The harness runs from main, so workflow/script changes take effect post-merge. # REBUILD=1: each Vast box is fresh, GPU-specific hardware β€” always rebuild both # binaries (cubin is compiled for the detected arch); never trust a cached binary. @@ -493,7 +419,7 @@ jobs: git fetch --force origin main; $FETCH; \ git checkout -f origin/main; \ REBUILD=1 CUDARC_PIN=cuda-12080 SYSROOT_DIR=/opt/lambda-vm-sysroot BENCH_FEATURES='$BENCH_FEATURES' \ - WORKLOAD=$BENCH_WORKLOAD CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT $EPOCH_ENV \ + WORKLOAD=real CONTINUATIONS=1 EPOCH_SIZE_LOG2=$GPU_REAL_EPOCH_LOG2 \ scripts/bench_abba.sh $REF_A origin/main $PAIRS" # pipefail so a failed remote bench (e.g. a prove that dies) propagates through the diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 1c5012ba8..78dc4fd2e 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -157,12 +157,11 @@ is **~3 hours** on the one shared bench runner, which every other bench queues b Run it by hand when a paired test on a real workload is worth that; the option is here rather than a footgun on a comment trigger. -**Why the GPU default differs from the CPU one.** `/bench-gpu` rents its own box per -run, so the cost is money rather than queue time on a runner every other bench is -waiting for. That removes the reason to keep the real block opt-in, so there it is -the default and the synthetic fixtures stay reachable via the existing `cont[TX]` / -`mono[TX]` tokens β€” old numbers reproduce with the exact syntax that produced them. -On the CPU side the shared runner makes the opposite trade correct. +**Both bench flows prove this block.** `/bench` runs it sampled on the shared +runner, against the cached baseline main publishes; `/bench-gpu [N]` runs it as +N A/B/B/A pairs on a rented box, comparing PR vs main on the same machine β€” +absolute GPU times are host-CPU-dependent, so only same-box deltas are +meaningful. **GPU baseline (measured), and why the GPU epoch is 2^22.** On an RTX 5090 (32,607 MiB) against main @ `9ccdaf2`, same fixture and CLI, one prove per setting: