From d381499dc86622b0025765a1c159940c09b3c914 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 3 Aug 2026 16:31:47 -0300 Subject: [PATCH 01/46] Bump ethrex to 4f658c2b, regenerate fixtures --- Makefile | 2 +- crypto/ethrex-crypto/Cargo.lock | 68 +------------- crypto/ethrex-crypto/Cargo.toml | 2 +- executor/programs/rust/ethrex/Cargo.lock | 46 ++++++---- executor/programs/rust/ethrex/Cargo.toml | 11 ++- executor/tests/README.md | 10 +-- executor/tests/ethrex_10_transfers.bin | Bin 14671 -> 13474 bytes executor/tests/ethrex_bench_4.bin | Bin 17371 -> 16174 bytes executor/tests/ethrex_empty_block.bin | Bin 9723 -> 8526 bytes executor/tests/ethrex_simple_tx.bin | Bin 12745 -> 11548 bytes tooling/ethrex-block-converter/Cargo.lock | 66 ++++++++------ tooling/ethrex-block-converter/Cargo.toml | 16 ++-- tooling/ethrex-block-converter/README.md | 41 +++++++-- tooling/ethrex-block-converter/src/main.rs | 32 +++++-- tooling/ethrex-fixtures/Cargo.lock | 100 +++++++++------------ tooling/ethrex-fixtures/Cargo.toml | 10 +-- tooling/ethrex-tests/Cargo.lock | 66 +++++--------- tooling/ethrex-tests/Cargo.toml | 2 +- tooling/ethrex-tests/tests/ethrex.rs | 10 ++- 19 files changed, 225 insertions(+), 257 deletions(-) diff --git a/Makefile b/Makefile index 25dce43de..098a04296 100644 --- a/Makefile +++ b/Makefile @@ -308,7 +308,7 @@ test-rust: compile-programs-rust 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 +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 0a301731b84515260c2ad7779fef3e4ef8b2424fbcc055ee2f5a224bc88120ab # 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 diff --git a/crypto/ethrex-crypto/Cargo.lock b/crypto/ethrex-crypto/Cargo.lock index ec809fff9..8747b05a0 100644 --- a/crypto/ethrex-crypto/Cargo.lock +++ b/crypto/ethrex-crypto/Cargo.lock @@ -168,18 +168,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[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" @@ -189,19 +177,6 @@ dependencies = [ "generic-array", ] -[[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 = "byteorder" version = "1.5.0" @@ -391,15 +366,13 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "ethereum-types", - "ff", "hex-literal", "k256", "num-bigint", @@ -416,7 +389,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -438,12 +410,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "generic-array" version = "0.14.9" @@ -636,15 +602,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group", -] - [[package]] name = "paste" version = "1.0.15" @@ -703,12 +660,6 @@ 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 = "rand" version = "0.8.6" @@ -919,12 +870,6 @@ dependencies = [ "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" @@ -1031,15 +976,6 @@ 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.52" diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml index ea6c91074..5310c00b0 100644 --- a/crypto/ethrex-crypto/Cargo.toml +++ b/crypto/ethrex-crypto/Cargo.toml @@ -18,7 +18,7 @@ license = "MIT OR Apache-2.0" # Defines the `Crypto` trait, `CryptoError`, and `keccak::keccak_hash`. Same rev # + `default-features = false` as the guest's ethrex-crypto, so feature # unification adds nothing to the guest build (no C secp256k1 / malachite / kzg). -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-crypto", default-features = false } # Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 # (a version split would make `FieldElement`/`Scalar` incompatible types). # `expose-field` is required by the x-only reconstruction. diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index e1674f74f..df0a1bb6f 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -721,8 +721,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crc32fast", @@ -749,17 +749,16 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "ethereum-types", - "ff", "hex-literal", "k256", + "malachite", "num-bigint", "p256", "ripemd", @@ -770,9 +769,10 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ + "bls12_381", "bytes", "ethereum-types", "ethrex-common", @@ -780,6 +780,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-rlp", "ethrex-vm", + "ff", "hex", "k256", "lambda-vm-syscalls", @@ -791,8 +792,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -810,8 +811,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -827,8 +828,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -837,8 +838,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -846,18 +847,19 @@ dependencies = [ "ethereum-types", "ethrex-crypto", "ethrex-rlp", - "lazy_static", + "hashbrown 0.15.5", "rayon", "rkyv", "rustc-hash", "serde", + "spin", "thiserror 2.0.18", ] [[package]] name = "ethrex-vm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -2047,6 +2049,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 4922712dd..58d63cd5f 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -25,13 +25,16 @@ lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # crypto defaults); ethrex's own `LambdaVmCrypto` compiles unused — we inject our # own. KZG is NOT linked under this feature, so the point-evaluation precompile # (0x0a) is unsupported — see src/main.rs. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } # Exact pin: must match the fixture writer (tooling/ethrex-fixtures) and the # executor test reader so the rkyv ProgramInput layout stays consistent. rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } -# `ethrex-guest-program`'s `lambdavm` feature pins `lambda-vm-syscalls` to an -# older commit. Override it with our working-tree copy so the guest links our -# current syscalls (keccak_permute + the Print-ecall no-op fix). +# `ethrex-guest-program`'s `lambdavm` feature git-deps `lambda-vm-syscalls` at a +# pinned commit of THIS repo, which upstream bumps on its own schedule. Override it +# with the working-tree copy so the guest always links the same syscalls as the rest +# of the repo — including uncommitted local edits — rather than whatever commit +# ethrex last pinned. (At the current ethrex rev the two happen to coincide; this +# override is what keeps that coincidence from being load-bearing.) [patch."https://github.com/yetanotherco/lambda_vm.git"] lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/tests/README.md b/executor/tests/README.md index fdbf47bcb..ceee3c58d 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -15,7 +15,7 @@ merges to `main`): ```text https://github.com/lambdaclass/ethrex.git -156cb8d6a3974f411d71622eecd1b249ee37ff1c +4f658c2b3d10e3f21d35ce546870f55ca3f940fc ``` ### Generation @@ -41,15 +41,15 @@ Known fixtures: ```text ethrex_empty_block.bin - sha256: d3e594f07cc74e4ddc9db9e9db220a65a2d2e578b619fc3ce06e346007b3ca43 + sha256: ca7454142f13db5d04356366e3917d264cbea709ddd1b5e6526720dbaa12064d contents: stateless ethrex empty block ProgramInput (0 transactions) ethrex_simple_tx.bin - sha256: 15e3b3efa434186682537755d828ac8bbdde4be3fc7cbe34f26687b618a6c6ab + sha256: 29c1607297d21d88accb33767c8e03eb6536d746e6b2399930afdc9d67f1fe3f contents: stateless ethrex block with one plain ETH transfer transaction ethrex_10_transfers.bin - sha256: 38901ee4d40b99cf0aa7f642a92f0fc8db76d974bf43033a1673839020c3c28e + sha256: d04cfed35bd16c8248ab8ebf2f3ca2ff01c08269271b17e5d5db3b1f22ea03ad contents: stateless ethrex block with ten plain ETH transfer transactions ``` @@ -58,7 +58,7 @@ ethrex_10_transfers.bin 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` downloads -`ethrex_mainnet_25368371.bin` (1,110,156 B) from the `bench-fixtures-v1` release +`ethrex_mainnet_25368371.bin` (1,110,165 B) from the `bench-fixtures-v1` release and verifies it against `ETHREX_REAL_BLOCK_FIXTURE_SHA256` in the Makefile before moving it into place. It is gitignored rather than committed, so the checksum lives next to the URL in the Makefile rather than in the table above (the checksum diff --git a/executor/tests/ethrex_10_transfers.bin b/executor/tests/ethrex_10_transfers.bin index 8b6c89182dff52ad084c2a5fd0af30b9357ffd07..dd9303cfe4167942b6d264c42058a2ddc6c3c544 100644 GIT binary patch delta 160 zcmX?Kv?y~!x&bpo?~}^f4TGjKMRn~yT3WyoRM*|gC^JHx8@v^1uQrwXIiXb`u`hfiNRz8E)D=H C)J6vY delta 440 zcmZ3KdA?{vy1`@x4Hbn+->MnE9y~6Yy!V+#@?D{`TaBLCaGY9Jm$)>1x~SSzrp<=3 z0*sRb#5)xkp4|QUt)Q@CPvH@VEH1u$)#dhv(;4z7Eacd~BsWFl?f=Pw!b+3hidefa zGO&Bie0k1Zp4nD)XLm!F$Q7^Wfx*n&%WW|EXMGU9!!gcUxv;tU0fT!PR4T=U1|)Z#{ol zgs0H^)?`Hy6_-*rv*W^@vWxvqCaH3VEttRAea=Jn(z$wXTP=3Xzm)ozk>UEJdvCOp zl|-UW=SJn<{Nn%rj0_A6b~pb2X93bXZf#y@ c#K<_=fQw`DZZ6KrdyLmGar^=5Gyn+!0D^0|W&i*H diff --git a/executor/tests/ethrex_bench_4.bin b/executor/tests/ethrex_bench_4.bin index 45fe930380e9f02aedaa1aeb681926704d058346..6b716bfc9002715b7c010a4d17072122742f5b75 100644 GIT binary patch delta 176 zcmccJ&bY2_!)GgIhQC)PKh%()eA?cX>GUly)6hYgDfSVNslc@2Iap-6gDcaHH(-$+ z@4-~@XRw5$qcYQ+A0QFN50gI{DcCSFOf|p7{hP%+Y_iLMh%%ZEZf`i9A%DU`j{Qq=Q#9WGpX?~2G}%_t z#+iZLYv#*y_VUcOsyn+Ix=eP~?Gov4cZ_@}cO^AW^ps5^BZH$@&+O72Q~yRuKYt`8 zs(x40Iim5hNOs`f`0Rz!oty_j7Ar9_X#P{VzPeVrFGMN0E{7^$4XrbKg{U5eIuAFgR-t39P zf#7Q%_7cBVtn2<lvuq+cp>tsVErO9sB2+F2@VWn4>6(*Ge zld&1hlkgJFApqpv`Sx&ec)W4MKxzsOZzZQcF_!>ukf8{_q-|s#83~@`^ld%;f zM0Ex>#}kPiqfa!FB@0%dpR-4t;Rkh{F71UlxS!Hx;{gEIlHTkqV^1HsnCSQ`Sg782YHLi}QRTW?N!0 zna7iH3Ph6-6*~g}&+3zK86lH59YX@v?~`yDA(NdQLjv9OlW-X!lLH>h0bG;e9U_zQ z9v}k%&i#`>4nSt6{Rm_#Fj;&Di?r%>GQIo(X6oxJ zPiEp(o?NQ0V8h5T)%+IsZx-{g$u7?xR%=M+FSp^pa#j1uuGzcaZM^m8_?8EM|NmzI rg0olu|7Qf!U+(<>&jO_L?r*-R&&W7AftzdcWrICT|9?-OXlMZdNMt_< delta 437 zcmbOebuxLwLygG_{K|?9lfG3mem!_xGI{SajpVyRXSW(Xv*9?ktS)hB_;gXVsZ5gv z#a01TzE@;;a`)%Ag2IYDg-0B+xcKr_m)jdoXULzhkYoRn+!T$s|0gHP#XGTk&3t*z zUY^-jb!T@&m&wk$T_XMMj*$=LuB66^p0Y_~WN;MgnO(YL>fb2o=Z~aB)$fWrM>Jj* z$qw8bpS@7Jlk*_dLZH($|EXMGU9!!gcUxv;tU0fT!PR4T=U1|)Z#{olgs0H^*5r@e zDz2q$X2*p)Wf%LKOj6|zTQGmK`<#dDrE~S(wp#3%e<}4bBg6Gc_ugnHD~Uv%&Y8NK zvt2+YrbW)=v4pGHo`CwP(_(xXO#V$)l$2LuWRSbP|HIbDl{3!En>}$j5PZ$UUgFn^ zb={w96w{)U3}+sn%qZ$I*+I~T>H2+;j>*iLu1rgxf|;3`%1q{OflLLarcYpz^O~JZ z#$Uk9N-brkn4e(gUM(dihI5k(c@=;LaR`ZD{QsYkfq}v9#{d5;Kzhfm&C=S8jFUHL Ub57RN*}}x}2dK+va-TLE0MX2`#sB~S diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index a8268a857..ca8f6b2cc 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -883,6 +883,7 @@ version = "0.1.0" dependencies = [ "ethrex-common", "ethrex-config", + "ethrex-crypto", "ethrex-guest-program", "k256", "lambda-vm-ethrex-crypto", @@ -894,8 +895,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crossbeam", @@ -916,8 +917,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crc32fast", @@ -944,8 +945,8 @@ dependencies = [ [[package]] name = "ethrex-config" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ethrex-common", "ethrex-p2p", @@ -956,16 +957,14 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "c-kzg", "ethereum-types", - "ff", "hex-literal", "k256", "malachite", @@ -979,9 +978,10 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ + "bls12_381", "bytes", "ethereum-types", "ethrex-common", @@ -989,6 +989,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-rlp", "ethrex-vm", + "ff", "hex", "k256", "lambda-vm-syscalls", @@ -1000,8 +1001,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -1019,8 +1020,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -1036,8 +1037,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ethrex-common", "serde", @@ -1048,8 +1049,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "aes", "aes-gcm", @@ -1090,8 +1091,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -1100,8 +1101,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -1122,8 +1123,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -1131,18 +1132,19 @@ dependencies = [ "ethereum-types", "ethrex-crypto", "ethrex-rlp", - "lazy_static", + "hashbrown 0.15.5", "rayon", "rkyv", "rustc-hash", "serde", + "spin", "thiserror 2.0.19", ] [[package]] name = "ethrex-vm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -2725,6 +2727,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index f61e68a68..87550cd8a 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -9,8 +9,13 @@ 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", default-features = false } -ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config", default-features = false } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-common", default-features = false } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-config", default-features = false } +# Only for `NativeCrypto`, the host `Crypto` impl that `into_execution_witness` +# now takes. Nothing re-exports it, and this crate's own guest-crypto impl is a +# dev-dependency, so the bin build needs its own line. Already in the graph via +# `ethrex-config` → `ethrex-p2p`, so this adds no compilation. +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-crypto", default-features = false } # 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 @@ -21,7 +26,7 @@ ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156c # `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"] } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", 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 the guest @@ -55,7 +60,8 @@ lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" } # 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 -# older commit; override it with our working-tree copy, as the guest does. +# `ethrex-guest-program`'s `lambdavm` feature git-deps `lambda-vm-syscalls` at a +# pinned commit of this repo; override it with the working-tree copy, as the guest +# does, so both resolve the same syscalls. [patch."https://github.com/yetanotherco/lambda_vm.git"] lambda-vm-syscalls = { path = "../../syscalls" } diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 17cf8a2f8..268d6a0fb 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -88,11 +88,18 @@ Artifacts live in the **[`bench-fixtures-v1`][release]** release on | asset | sha256 | read by | |---|---|---| -| `ethrex_mainnet_25368371.bin` | `61eba49b…` | every benchmark (**current default**) | +| `ethrex_mainnet_25368371.bin` | `0a301731…` | 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 | +> **Re-upload pending.** The `0a301731…` above is what the Makefile verifies against +> after the ethrex `4f658c2b` bump, which moved the rkyv layout and so changed the +> fixture bytes. The hosted asset still carries the pre-bump `61eba49b…` until it is +> replaced in the release, so `make ethrex-real-block-fixture` fails its checksum on a +> clean checkout in the meantime — loudly, which is the designed failure mode. Only the +> `.bin` is stale; the caches are inputs and are unaffected. + 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 @@ -130,12 +137,15 @@ cargo run --release -- Output is deterministic for a given cache file: ```text -wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110156 bytes): 1 block(s) \ +wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110165 bytes): 1 block(s) \ from mainnet starting at #25368371, 29 transaction(s), 2428684 gas ``` -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. +Verified at the current ethrex rev: regenerating from the hosted cache reproduces +`0a301731…` byte for byte, and the result passes `test_ethrex_real_block_native` — +which is what proves the hosted cache and the fixture the Makefile expects describe +the same block. (Byte count and digest are both rev-dependent: the `4f658c2b` bump +moved them from 1,110,156 B / `61eba49b…`, the bytes still hosted in the release.) The converter's `conversion_is_reproducible` test enforces the same property, but against its own pinned block rather than this one — see [Validation](#validation). @@ -350,6 +360,15 @@ current default), so sizing a candidate from its gas mispredicts cost. **Current default — main-vintage (merge `fdb92f67`, main @ `9ccdaf2`):** +These figures were measured on the pre-bump fixture (`61eba49b…`, 1,110,156 B) and are +left as measured rather than restamped. The ethrex `4f658c2b` bump changed the fixture +bytes, so they are a baseline for a workload that no longer exists byte-for-byte. + +Post-bump CPU counterparts, measured ABBA on `vm-benchmarks-1` at the same epoch 2^22 +(see `ETHREX_BUMP_CYCLE_RESULTS.md`): **45,074,552 cycles** (−11.24%), **142.37 s** CPU +prove (−10.87%), **936.7 MB** proof (−12.22%), peak RSS flat at ~48 GB. The GPU column +has no post-bump counterpart yet. + | 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 | **158.8 s** @ epoch 2^22 (2.65x the GPU wall) | 1.15 GB CPU / 1.12 GB GPU | 1,110,156 B, `61eba49b…` | @@ -452,10 +471,16 @@ 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 -`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. +`main` while we pin a branch off it, and the type has diverged between the two +before: against the previously pinned rev (`156cb8d6…`) `main` carried an extra +`fee_configs` field and had moved the type from `l1::` to `input::`, so replay's +binary would not deserialize in our guest at all. + +At the currently pinned rev (`4f658c2b…`, rebased onto recent `main`) the +`ProgramInput` definition matches `main`'s again, and that gap has closed for +now. What has not closed: replay resolves rkyv itself from ethrex's `^0.8.10` +rather than our exact `=0.8.16`, `main` has no `lambdavm` feature to build the +guest side against, and the next bump can reopen the type gap without warning. 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 diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index f96b03f24..545c7cc6e 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -5,18 +5,23 @@ //! cargo run --release -- use ethrex_common::types::Block; -use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_common::types::block_execution_witness::{RpcExecutionWitness, decode_witness_headers}; use ethrex_config::networks::Network; +use ethrex_crypto::NativeCrypto; 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. +/// depending on `ethrex-replay`: it tracks ethrex `main`, we pin a branch off it, +/// and `ProgramInput` has diverged between the two before (it once carried an +/// extra `fee_configs` field and lived at a different module path). At the +/// currently pinned rev the type happens to match `main`'s again, but replay +/// still resolves rkyv itself from ethrex's `^0.8.10` against our exact `=0.8.16`, +/// and nothing stops the type drifting apart on the next bump. This JSON carries +/// only `blocks` + `witness` + `network` as plain serde, so it is the +/// version-tolerant interface between the two regardless. /// /// Extra fields in the file (L2 blob data, custom `chain_config`) are ignored. #[derive(Deserialize)] @@ -70,10 +75,19 @@ 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 inside `witness`. + // Those headers are now decoded by the caller rather than inside the call, and + // the `Crypto` argument is what recomputes the block hashes the parent lookup + // matches on. `NativeCrypto` is keccak-only here, so it produces the same bytes + // as the `LambdaVmEcsmCrypto` the guest injects (whose host path is also + // software keccak) — the fixture does not depend on which one converts it. let chain_config = cache.network.get_genesis()?.config; - let witness = cache - .witness - .into_execution_witness(chain_config, summary.first_block_number)?; + let decoded_headers = decode_witness_headers(&cache.witness.headers)?; + let witness = cache.witness.into_execution_witness( + chain_config, + summary.first_block_number, + &decoded_headers, + &NativeCrypto, + )?; Ok((ProgramInput::new(cache.blocks, witness), summary)) } @@ -216,7 +230,7 @@ mod tests { .map(|b| format!("{b:02x}")) .collect(); assert_eq!( - digest, "1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142", + digest, "6ffc80b25ec9ed3c203c35bdb8a4a05aff08d98b1a9c71648447bc199a552dc9", "fixture bytes changed — regenerate it and update the README checksum", ); } diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index 8d671d94c..1702880a9 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -389,19 +389,6 @@ 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.16" @@ -481,9 +468,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.7" +version = "2.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +checksum = "38d04308254695569fdb9bfe3bacc1c91837a670d0806605eb82d63748fbd3a6" dependencies = [ "blst", "cc", @@ -988,8 +975,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crossbeam", @@ -1010,8 +997,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crc32fast", @@ -1040,16 +1027,14 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "c-kzg", "ethereum-types", - "ff", "hex-literal", "k256", "malachite", @@ -1081,8 +1066,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -1100,8 +1085,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -1119,8 +1104,8 @@ dependencies = [ [[package]] name = "ethrex-l2-rpc" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "axum", "bytes", @@ -1142,6 +1127,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tokio-util", "tower-http", "tracing", "tracing-subscriber", @@ -1150,8 +1136,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -1168,8 +1154,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "axum", "ethrex-common", @@ -1184,8 +1170,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "aes", "aes-gcm", @@ -1228,8 +1214,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -1238,8 +1224,8 @@ dependencies = [ [[package]] name = "ethrex-rpc" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "axum", "axum-extra", @@ -1259,6 +1245,7 @@ dependencies = [ "hex-literal", "jsonwebtoken", "rand 0.8.6", + "rayon", "reqwest", "secp256k1", "serde", @@ -1277,8 +1264,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -1299,8 +1286,8 @@ dependencies = [ [[package]] name = "ethrex-storage-rollup" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "async-trait", "bincode", @@ -1315,8 +1302,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -1324,18 +1311,19 @@ dependencies = [ "ethereum-types", "ethrex-crypto", "ethrex-rlp", - "lazy_static", + "hashbrown 0.15.5", "rayon", "rkyv", "rustc-hash", "serde", + "spin", "thiserror 2.0.18", ] [[package]] name = "ethrex-vm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -1375,7 +1363,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -2541,15 +2528,6 @@ dependencies = [ "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" @@ -3543,6 +3521,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index 5e99f37d3..3320ba25b 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -9,11 +9,11 @@ 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-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-blockchain" } -ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-storage" } -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common" } -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program" } -ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-l2-rpc" } +ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-blockchain" } +ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-storage" } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-common" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program" } +ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-l2-rpc" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # Exact pin: the fixture writer and the guest/executor readers must agree on the diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 250e2411f..eafb4bdff 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -247,19 +247,6 @@ dependencies = [ "generic-array", ] -[[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", - "subtle", -] - [[package]] name = "bs58" version = "0.5.1" @@ -706,8 +693,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "crc32fast", @@ -736,15 +723,13 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "ethereum-types", - "ff", "hex-literal", "k256", "malachite", @@ -759,8 +744,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -778,8 +763,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -797,8 +782,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -815,8 +800,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "ethereum-types", @@ -834,8 +819,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "anyhow", "bytes", @@ -843,18 +828,19 @@ dependencies = [ "ethereum-types", "ethrex-crypto", "ethrex-rlp", - "lazy_static", + "hashbrown 0.15.5", "rayon", "rkyv", "rustc-hash", "serde", + "spin", "thiserror 2.0.18", ] [[package]] name = "ethrex-vm" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "bytes", "derive_more", @@ -885,7 +871,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core", "subtle", ] @@ -1434,15 +1419,6 @@ dependencies = [ "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" @@ -1951,6 +1927,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" diff --git a/tooling/ethrex-tests/Cargo.toml b/tooling/ethrex-tests/Cargo.toml index f4774278d..5ff8ef39f 100644 --- a/tooling/ethrex-tests/Cargo.toml +++ b/tooling/ethrex-tests/Cargo.toml @@ -12,7 +12,7 @@ edition = "2024" executor = { path = "../../executor" } # Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch) # so the native reference reads the same ProgramInput rkyv layout. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program" } # Exact pin: the fixture writer and the guest/executor readers must agree on the # rkyv layout. Keep this in sync with tooling/ethrex-fixtures and # executor/programs/rust/ethrex/Cargo.toml. diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 6374f2f43..9ad1ee786 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -127,11 +127,13 @@ fn test_ethrex_real_block_native() { /// 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. +/// test goes red, which is the safe direction — and it has: the 4f658c2b rev bump +/// dropped `openvm-kzg` from the sentence, so the expected text moved with it. /// /// 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. +/// is `["std", "kzg-rs", "secp256k1", "aws-lc-rs", "blst"]`, 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}; @@ -141,7 +143,7 @@ fn no_kzg_backend_linked() { Err(err) => format!("{err:?}"), }; assert!( - message.contains("One of features c-kzg, openvm-kzg or kzg-rs should be active"), + message.contains("One of features c-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 4657bbbce5a2515882b9dbe82df88d88cc89facf Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 3 Aug 2026 16:33:18 -0300 Subject: [PATCH 02/46] Cover ethrex_bench_4.bin in the checksum gate --- Makefile | 3 ++- executor/tests/README.md | 12 ++++++++++++ tooling/ethrex-fixtures/update_readme_checksums.py | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 098a04296..9fd34b10f 100644 --- a/Makefile +++ b/Makefile @@ -499,7 +499,8 @@ regen-ethrex-fixtures: cd tooling/ethrex-fixtures && \ cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin && \ cargo run --release -- 1 ../../executor/tests/ethrex_simple_tx.bin && \ - cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin + cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin && \ + cargo run --release -- 4 ../../executor/tests/ethrex_bench_4.bin distinct $(MAKE) update-ethrex-fixture-checksums update-ethrex-fixture-checksums: diff --git a/executor/tests/README.md b/executor/tests/README.md index ceee3c58d..ae35e6135 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -30,8 +30,15 @@ cd tooling/ethrex-fixtures cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin # empty block cargo run --release -- 1 ../../executor/tests/ethrex_simple_tx.bin # 1 transfer cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin # 10 transfers +cargo run --release -- 4 ../../executor/tests/ethrex_bench_4.bin distinct # recursion profile ``` +`ethrex_bench_4.bin` is the odd one out: `distinct` mode, and it is read by the +recursion profile target rather than the executor tests (see the Makefile's +`recursion-profile-block-input`). It is committed like the rest, so it is +regenerated and checksummed with them — a rev bump makes every one of these +undecodable, not just the three the executor reads. + To regenerate after an ethrex rev bump, update the `rev` in `tooling/ethrex-fixtures/Cargo.toml` (and the guest's), then run `make regen-ethrex-fixtures` from the repo root. The target rebuilds the @@ -51,6 +58,11 @@ ethrex_simple_tx.bin ethrex_10_transfers.bin sha256: d04cfed35bd16c8248ab8ebf2f3ca2ff01c08269271b17e5d5db3b1f22ea03ad contents: stateless ethrex block with ten plain ETH transfer transactions + +ethrex_bench_4.bin + sha256: bb26e6d6595f537c2fd57bfd59da6abe82241bd92690a21c2225da94d5cf507a + contents: stateless ethrex block with four plain ETH transfers, `distinct` mode + (N senders -> N recipients); read by the recursion profile target ``` ## Real-block fixtures diff --git a/tooling/ethrex-fixtures/update_readme_checksums.py b/tooling/ethrex-fixtures/update_readme_checksums.py index 76997dbe1..f9fbee7fb 100644 --- a/tooling/ethrex-fixtures/update_readme_checksums.py +++ b/tooling/ethrex-fixtures/update_readme_checksums.py @@ -12,6 +12,10 @@ "ethrex_empty_block.bin", "ethrex_simple_tx.bin", "ethrex_10_transfers.bin", + # Committed like the three above, but generated in `distinct` mode and read by + # the recursion profile target rather than the executor tests. Omitting it left + # a committed fixture that a rev bump silently staled with nothing to catch it. + "ethrex_bench_4.bin", ) From 03d3ffed5be8ecb6b1390914b343f1500cf83f8a Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 3 Aug 2026 17:58:44 -0300 Subject: [PATCH 03/46] Point the real-block fixture at the post-bump asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rkyv layout moved with the ethrex rev, so the block's fixture did too, and the release now carries both: the pre-bump bytes under the original name and the post-bump ones under a name carrying the rev. Uploading under a new name rather than replacing the asset keeps `main` working while this branch is open — its Makefile still pins the pre-bump sha256, and a replaced asset would fail that checksum on every push, taking the published benchmark baseline with it. --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9fd34b10f..5adc8a44f 100644 --- a/Makefile +++ b/Makefile @@ -307,7 +307,11 @@ test-rust: compile-programs-rust # 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 +# The asset name carries the ethrex rev because the bytes are a function of it: the +# archived ProgramInput layout moves with the pin, so one block has one fixture per rev. +# Uploading under a new name rather than replacing the old one keeps `main` — which still +# expects the pre-bump sha256 — fetching its own artifact while this branch is open. +ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371_4f658c2b.bin ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 0a301731b84515260c2ad7779fef3e4ef8b2424fbcc055ee2f5a224bc88120ab # 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). From 55b400cf1bbd0c57b029d6081449d524da6f90d6 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Mon, 10 Aug 2026 16:20:14 -0300 Subject: [PATCH 04/46] docs(ethrex): point the fixture docs at the rev-suffixed asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Makefile fetches ethrex_mainnet_25368371_4f658c2b.bin (the suffixed upload keeps main's pre-bump checksum working while the branch is open), but the converter README's asset table still listed the un-suffixed name with the post-bump checksum next to a 'Re-upload pending' note that the suffixed upload made moot, and executor/tests/README.md named the un-suffixed asset with the post-bump byte count. Also drop the dangling ETHREX_BUMP_CYCLE_RESULTS.md reference — the numbers are quoted inline. --- executor/tests/README.md | 2 +- tooling/ethrex-block-converter/README.md | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/executor/tests/README.md b/executor/tests/README.md index ae35e6135..fc6b3f8a8 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -70,7 +70,7 @@ ethrex_bench_4.bin 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` downloads -`ethrex_mainnet_25368371.bin` (1,110,165 B) from the `bench-fixtures-v1` release +`ethrex_mainnet_25368371_4f658c2b.bin` (1,110,165 B) from the `bench-fixtures-v1` release and verifies it against `ETHREX_REAL_BLOCK_FIXTURE_SHA256` in the Makefile before moving it into place. It is gitignored rather than committed, so the checksum lives next to the URL in the Makefile rather than in the table above (the checksum diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 268d6a0fb..48c58e27b 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -88,17 +88,16 @@ Artifacts live in the **[`bench-fixtures-v1`][release]** release on | asset | sha256 | read by | |---|---|---| -| `ethrex_mainnet_25368371.bin` | `0a301731…` | every benchmark (**current default**) | +| `ethrex_mainnet_25368371_4f658c2b.bin` | `0a301731…` | 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 | -> **Re-upload pending.** The `0a301731…` above is what the Makefile verifies against -> after the ethrex `4f658c2b` bump, which moved the rkyv layout and so changed the -> fixture bytes. The hosted asset still carries the pre-bump `61eba49b…` until it is -> replaced in the release, so `make ethrex-real-block-fixture` fails its checksum on a -> clean checkout in the meantime — loudly, which is the designed failure mode. Only the -> `.bin` is stale; the caches are inputs and are unaffected. +> The asset name carries the ethrex rev because the bytes are a function of it: the +> archived `ProgramInput` rkyv layout moves with the pin, so one block has one +> fixture per rev. The pre-bump bytes stay hosted under the original name +> (`ethrex_mainnet_25368371.bin`, `61eba49b…`) so older `main`s — whose Makefile +> pins that sha256 — keep fetching their own artifact. 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 @@ -364,10 +363,9 @@ These figures were measured on the pre-bump fixture (`61eba49b…`, 1,110,156 B) left as measured rather than restamped. The ethrex `4f658c2b` bump changed the fixture bytes, so they are a baseline for a workload that no longer exists byte-for-byte. -Post-bump CPU counterparts, measured ABBA on `vm-benchmarks-1` at the same epoch 2^22 -(see `ETHREX_BUMP_CYCLE_RESULTS.md`): **45,074,552 cycles** (−11.24%), **142.37 s** CPU -prove (−10.87%), **936.7 MB** proof (−12.22%), peak RSS flat at ~48 GB. The GPU column -has no post-bump counterpart yet. +Post-bump CPU counterparts, measured ABBA on `vm-benchmarks-1` at the same epoch 2^22: +**45,074,552 cycles** (−11.24%), **142.37 s** CPU prove (−10.87%), **936.7 MB** proof +(−12.22%), peak RSS flat at ~48 GB. The GPU column has no post-bump counterpart yet. | block | gas | cycles | GPU prove (RTX 5090) | CPU prove | proof | fixture | |---|---|---|---|---|---|---| From d829a2cbb844f09c9d22ca6bdbaf8857c6e8859c Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 26 Aug 2026 17:10:30 -0300 Subject: [PATCH 05/46] Move the ethrex pin from 4f658c2b to 797df554, off the unmerged feat/lambdavm-prover-backend branch and onto ethrex main, and stop declaring the lambdavm feature. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requiring that feature is what tied the guest to the backend branch, since it exists nowhere else — and that branch is 69 commits behind main and has not moved since 2026-08-04, so waiting for it to merge was costing real performance. On the real benchmark block the guest goes from 34,241,608 to 30,498,818 cycles and from 120.715 s to 110.259 s of proving time (median of 3, spread under 0.8%), dropping from 9 epochs to 8; against main's pin the cycle count falls 22.9%. No feature replaces it. ethrex's per-zkVM features are not backend selectors — each is only a list of optional crypto dependencies, and none gates any method of the Crypto trait. Measured against the sibling feature activating the widest dependency set, ethrex's precompile stress fixtures come out identical to the cycle (stress_modexp_150M 6,373,285,966 and stress_alt_bn128_150M 22,986,061,145 both ways) with a smaller ELF. What makes this guest LambdaVM never travelled through a feature: lambda-vm-syscalls, lambda-vm-ethrex-crypto and the riscv64im-lambda-vm-elf target are direct, and every run still reports its 116 ECSM and 10,659 keccak precompile calls. 797df554 is the last commit where this is only a rev change. b5271885 rewrites the guest entry point to run_stateless_guest over SSZ input, which needs a new shim and a new fixture format; and between 797df554 and that boundary there is no perf(levm) work, while the two commits that matter (#7105, #7104) are already in. Release tags do not help — v24.0.0 predates both and is diverged from main, because ethrex cuts releases from a side branch. The rkyv ProgramInput layout moved with the rev, so every committed fixture is regenerated and the converter's reproducibility digest updated, as that test asks for on a legitimate bump. scripts/set_ethrex_rev.sh moves all 11 pins together: a guest and tooling on different revs do not fail to build, they produce a fixture the guest silently misreads. The real-block fixture asset for the new rev still has to be uploaded to the bench-fixtures-v1 release before CI can fetch it. --- BUMP_797df554_RESULTS.md | 130 +++++++++++++++++ Makefile | 4 +- crypto/ethrex-crypto/Cargo.toml | 2 +- executor/programs/rust/ethrex/Cargo.lock | 157 +++++++++------------ executor/programs/rust/ethrex/Cargo.toml | 49 +++++-- executor/tests/README.md | 8 +- executor/tests/ethrex_10_transfers.bin | Bin 13474 -> 13492 bytes executor/tests/ethrex_bench_4.bin | Bin 16174 -> 16192 bytes executor/tests/ethrex_empty_block.bin | Bin 8526 -> 8544 bytes executor/tests/ethrex_simple_tx.bin | Bin 11548 -> 11566 bytes scripts/set_ethrex_rev.sh | 87 ++++++++++++ tooling/ethrex-block-converter/Cargo.lock | 129 ++++++++++------- tooling/ethrex-block-converter/Cargo.toml | 8 +- tooling/ethrex-block-converter/src/main.rs | 2 +- tooling/ethrex-fixtures/Cargo.lock | 109 ++++++++++---- tooling/ethrex-fixtures/Cargo.toml | 10 +- tooling/ethrex-tests/Cargo.lock | 84 ++++++++--- tooling/ethrex-tests/Cargo.toml | 2 +- 18 files changed, 564 insertions(+), 217 deletions(-) create mode 100644 BUMP_797df554_RESULTS.md create mode 100755 scripts/set_ethrex_rev.sh diff --git a/BUMP_797df554_RESULTS.md b/BUMP_797df554_RESULTS.md new file mode 100644 index 000000000..21a6b1525 --- /dev/null +++ b/BUMP_797df554_RESULTS.md @@ -0,0 +1,130 @@ +# Bumping the ethrex pin to `797df554` + +Moves the guest from `4f658c2b` (2026-07-31, on the unmerged +`feat/lambdavm-prover-backend` branch) to `797df554` (2026-08-07, on ethrex +`main`). + +## Results + +Real mainnet block 25368371 — the fixture `bench_abba.sh`, `bench_verify.sh`, +`perf_diff.sh` and `benchmark-pr.yml` all resolve through +`make print-real-block-fixture`. + +| | before (`4f658c2b`) | after (`797df554`) | delta | +|---|---|---|---| +| **Proving time** (median of 3) | **120.715 s** | **110.259 s** | **−8.66%** | +| run spread | 0.681 s (0.56%) | 0.821 s (0.74%) | | +| Executed cycles | 34,241,608 | 30,498,818 | −10.93% | +| **Epochs** (2^22) | **9** | **8** | −1 | +| Peak heap | 47.3 GB | 46.2 GB | −2.4% | + +10.46 s off every proof, against a run spread below 0.8% on both sides — the +delta is roughly twelve times the noise. + +Against `main`'s pin (`156cb8d6`, 2026-05-22) the guest goes from **39,563,400** +to **30,498,818** cycles, **−22.9%**. + +### Time does not track cycles + +−10.93% of cycles buys −8.66% of proving time, and that gap is structural rather +than measurement error: proving cost also carries per-epoch fixed work and table +rows that do not shrink with the instruction count. Quoting a cycle delta as if it +were a time delta will overpromise. + +The epoch count is the clearest instance. At 2^22 cycles per epoch, 34.2M needed 9 +and 30.5M fits in 8, and that single step accounts for much of the 10 s. It also +means the next increment of cycle savings returns less until it drops the ninth +epoch — the benefit arrives in steps, not smoothly. + +## What changed + +**The pin**, in all 11 places (`scripts/set_ethrex_rev.sh` moves them together, so +the guest and the fixture tooling cannot drift apart — a mismatch there does not +fail to build, it produces a fixture the guest silently misreads). + +**The `lambdavm` feature is gone**, and not replaced. `ethrex-guest-program` +carries one feature per zkVM and none of them selects a backend: each is only a +list of optional crypto dependencies, and none gates any method of the `Crypto` +trait (its gates are `secp256k1`, `c-kzg`, `blst`, `std`). Measured against the +sibling feature that activates the widest dependency set, ethrex's own precompile +stress fixtures come out identical to the cycle — `stress_modexp_150M` +6,373,285,966 and `stress_alt_bn128_150M` 22,986,061,145 both ways — with the real +block at 30,498,818 vs 30,501,620 and a 2,928-byte smaller ELF. + +Requiring `lambdavm` is what tied the guest to the backend branch, since that is +the only place it exists. What makes this guest LambdaVM never travelled through +it: `lambda-vm-syscalls`, `lambda-vm-ethrex-crypto` and the +`riscv64im-lambda-vm-elf` target are direct, and every run above reports 116 ECSM +and 10,659 keccak precompile calls — the injected crypto is live on both sides. + +**Fixtures regenerated.** The rkyv `ProgramInput` layout moved with the rev, so +every committed `.bin` from the old pin fails to deserialize. All four synthetic +fixtures plus the real block were rebuilt; the converter's reproducibility digest +was updated, which is what that test asks for on a legitimate rev bump. + +| fixture | cycles | ECSM | +|---|---|---| +| `ethrex_empty_block` | 431,162 | 0 | +| `ethrex_simple_tx` | 653,263 | 4 | +| `ethrex_bench_4` | 1,047,001 | 16 | +| `ethrex_10_transfers` | 1,286,761 | 40 | +| real block 25368371 | 30,498,818 | 116 | + +## Why `797df554` and not something newer + +`b5271885` (2026-08-14) rewrites the guest entry point from `ProgramInput` / +`execution_program` to `run_stateless_guest` taking schema-prefixed SSZ +`statelessInputBytes`. Past that commit this is no longer a pin bump — it needs a +new guest shim and a new fixture format. `797df554` is the last point where the +bump is only the rev. + +Within that window it is also the right end of it: between 2026-08-07 and the +boundary there are 14 commits and **no `perf(levm)`**. The two that matter — +`perf(levm): monomorphize the dispatch loop on the validation observer` (#7105) +and `perf(l1): give branch-node hashing a monomorphic RLP encoder` (#7104) — are +already in. + +Release tags do not help here. `v24.0.0`'s commit predates both, and it is +*diverged* from `main` (4 commits of its own, 66 behind) because ethrex cuts +releases from a side branch rather than from the tip — so a tag can lack work that +is already merged. `v25.0.0` is past the API boundary. + +## Method + +`vm-benchmarks-1`, 96 cores / 125 GB, rustc 1.94.0. The real-block step of +`.github/workflows/benchmark-pr.yml` verbatim: + +``` +cli prove $ELF --private-input $REAL_INPUT \ + --continuations --epoch-size-log2 22 -o /tmp/real_proof.bin --time +``` + +3 runs per side, reported as median plus spread. + +Both sides started on an idle box (load 0.61 and 1.99). The baseline was held back +until load fell below 3 after the first side finished — starting it hot would have +inflated the baseline and flattered the bump. + +Each side used **its own canonical fixture**: the baseline fetched the published +`_4f658c2b` release asset, the bumped side used the newly generated `_797df554` +one. They cannot share a file — the rkyv layout differs — and each side's own +asset is what CI would fetch, which is the honest comparison rather than two +ad-hoc conversions. + +Cycle counts are deterministic: the bumped side reported 30,498,818 identically on +a laptop and on the bench box. + +Raw logs in `bench_ethrex_bump_20260826/`. + +## Before merging + +`ethrex_mainnet_25368371_797df554.bin` (in the repo root) must be uploaded to the +`bench-fixtures-v1` release of **yetanotherco/lambda_vm**. The Makefile already +points at it with its sha256; until the asset exists, `make +ethrex-real-block-fixture` works locally (the file is in place and verifies) but +CI cannot fetch it. + +This also stops being a routine bump: it leaves `feat/lambdavm-prover-backend` and +consumes ethrex `main` as a library. That branch is 69 commits behind `main` and +has not moved since 2026-08-04, so waiting for it to merge was itself costing the +−22.9% above. The PR's existing approval predates this change. diff --git a/Makefile b/Makefile index b1f13c9e6..9c4c096a1 100644 --- a/Makefile +++ b/Makefile @@ -311,8 +311,8 @@ ETHREX_REAL_BLOCK := 25368371 # archived ProgramInput layout moves with the pin, so one block has one fixture per rev. # Uploading under a new name rather than replacing the old one keeps `main` — which still # expects the pre-bump sha256 — fetching its own artifact while this branch is open. -ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371_4f658c2b.bin -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 0a301731b84515260c2ad7779fef3e4ef8b2424fbcc055ee2f5a224bc88120ab +ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371_797df554.bin +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 573004e62e3680a00d3cdbae19dc4897e2ec60d6ec0c1d05d9ef118cb8aef17f # 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 diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml index 5310c00b0..09d53ac3b 100644 --- a/crypto/ethrex-crypto/Cargo.toml +++ b/crypto/ethrex-crypto/Cargo.toml @@ -18,7 +18,7 @@ license = "MIT OR Apache-2.0" # Defines the `Crypto` trait, `CryptoError`, and `keccak::keccak_hash`. Same rev # + `default-features = false` as the guest's ethrex-crypto, so feature # unification adds nothing to the guest build (no C secp256k1 / malachite / kzg). -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-crypto", default-features = false } # Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 # (a version split would make `FieldElement`/`Scalar` incompatible types). # `expose-field` is required by the x-only reconstruction. diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index 93a5d800d..63705e473 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -189,22 +189,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bitcoin-io" -version = "0.1.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" - -[[package]] -name = "bitcoin_hashes" -version = "0.14.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - [[package]] name = "bitvec" version = "1.0.1" @@ -226,19 +210,6 @@ dependencies = [ "generic-array", ] -[[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 = "bs58" version = "0.5.1" @@ -697,8 +668,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crc32fast", @@ -712,6 +683,10 @@ dependencies = [ "indexmap 2.14.0", "lazy_static", "libc", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "lru", "once_cell", "rkyv", @@ -725,8 +700,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ark-bn254", "ark-ec", @@ -745,10 +720,9 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ - "bls12_381", "bytes", "ethereum-types", "ethrex-common", @@ -756,10 +730,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-rlp", "ethrex-vm", - "ff", "hex", - "k256", - "lambda-vm-syscalls", "rkyv", "serde", "serde_with", @@ -768,8 +739,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -778,7 +749,6 @@ dependencies = [ "k256", "lambdaworks-crypto", "rkyv", - "secp256k1", "serde", "serde_with", "thiserror 2.0.18", @@ -787,14 +757,15 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", "ethrex-common", "ethrex-crypto", "ethrex-rlp", + "libssz", "malachite", "rustc-hash", "serde", @@ -804,8 +775,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -814,8 +785,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -834,8 +805,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", @@ -856,7 +827,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -1019,15 +989,6 @@ 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-literal" version = "0.4.1" @@ -1284,6 +1245,47 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libssz" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "libssz-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libssz-merkle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +dependencies = [ + "libssz", + "sha2", +] + +[[package]] +name = "libssz-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +dependencies = [ + "libssz", + "libssz-merkle", + "smallvec", +] + [[package]] name = "log" version = "0.4.32" @@ -1429,15 +1431,6 @@ dependencies = [ "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" @@ -1861,26 +1854,6 @@ dependencies = [ "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.6", - "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.228" @@ -2005,6 +1978,12 @@ 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 = "spin" version = "0.9.9" diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 58d63cd5f..1d2ef0f4a 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -18,20 +18,49 @@ lambda-vm-syscalls = { path = "../../../../syscalls" } # lambda_vm repo and injected in src/main.rs — so crypto changes stay in our repo # and don't require an ethrex PR. lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } -# Pinned by immutable `rev` to a commit on the open LambdaVM-backend PR branch -# (feat/lambdavm-prover-backend) of ethrex; re-pin to the merge commit once it -# lands on ethrex `main`. The `lambdavm` feature is kept only for its dependency -# wiring (`ProgramInput`/`execution_program`/`ProgramOutput::encode` + pure-Rust -# crypto defaults); ethrex's own `LambdaVmCrypto` compiles unused — we inject our -# own. KZG is NOT linked under this feature, so the point-evaluation precompile -# (0x0a) is unsupported — see src/main.rs. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } +# Pinned by immutable `rev` to a commit on ethrex `main`. It used to point at the +# open LambdaVM-backend PR branch (`feat/lambdavm-prover-backend`) and wait for +# that to merge; it no longer does, because the branch stopped moving 69 commits +# behind `main` while guest performance work kept landing there. Measured on the +# real benchmark block, that wait had grown to -22.9%. +# +# No `features = [...]`, deliberately — not even `lambdavm`. +# +# `ethrex-guest-program` carries one feature per zkVM and, despite the names, none +# of them selects a backend: each is only a list of optional crypto dependencies +# to activate, and none gates any method of the `Crypto` trait (its gates are +# `secp256k1`, `c-kzg`, `blst`, `std`). `lambdavm` was +# `["dep:k256", "dep:lambda-vm-syscalls", "ethrex-l2-common/secp256k1"]`, and +# since #702 moved the `Crypto` impl into this repo the syscalls it pulled are for +# ethrex's own provider, which we no longer use — while this guest already depends +# on `lambda-vm-syscalls` by path, three lines above. +# +# What makes this guest LambdaVM is those path deps plus the +# `riscv64im-lambda-vm-elf` target. None of it travels through a feature: every +# run reports its ECSM and keccak precompile calls, which is what actually proves +# the injected crypto is live. +# +# Verified rather than assumed. Against the sibling feature activating the widest +# dependency set, ethrex's own precompile stress fixtures come out identical to +# the cycle — `stress_modexp_150M` 6,373,285,966 and `stress_alt_bn128_150M` +# 22,986,061,145 both ways — and the real benchmark block is 30,498,818 here vs +# 30,501,620 there, with a 2,928-byte smaller ELF. The difference is dead +# `substrate-bn`/`bls12_381` that the arkworks-based trait defaults never call. +# +# Declaring nothing is also what frees the pin: `lambdavm` exists ONLY on that +# unmerged branch, so requiring it is precisely what tied this guest to it. See +# scripts/bench_ethrex_pr.sh, which re-pins across arbitrary revs to measure +# upstream guest PRs. +# +# KZG is not linked, so the point-evaluation precompile (0x0a) is unsupported — +# see src/main.rs. +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program", default-features = false } # Exact pin: must match the fixture writer (tooling/ethrex-fixtures) and the # executor test reader so the rkyv ProgramInput layout stays consistent. rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } -# `ethrex-guest-program`'s `lambdavm` feature git-deps `lambda-vm-syscalls` at a -# pinned commit of THIS repo, which upstream bumps on its own schedule. Override it +# `lambda-vm-syscalls` can still reach the graph as a git dep pinned to a commit +# of THIS repo, which upstream bumps on its own schedule. Override it # with the working-tree copy so the guest always links the same syscalls as the rest # of the repo — including uncommitted local edits — rather than whatever commit # ethrex last pinned. (At the current ethrex rev the two happen to coincide; this diff --git a/executor/tests/README.md b/executor/tests/README.md index fc6b3f8a8..6ecbc7212 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -48,19 +48,19 @@ Known fixtures: ```text ethrex_empty_block.bin - sha256: ca7454142f13db5d04356366e3917d264cbea709ddd1b5e6526720dbaa12064d + sha256: 8d6f6061c71c23fad1d5dee26242d631efe0bff8d7f49422c2ba4cde9d4be919 contents: stateless ethrex empty block ProgramInput (0 transactions) ethrex_simple_tx.bin - sha256: 29c1607297d21d88accb33767c8e03eb6536d746e6b2399930afdc9d67f1fe3f + sha256: c40bce364f22758ab7fa6fe8b45ce4c305dee5add4536ef6dca0e74e410e2729 contents: stateless ethrex block with one plain ETH transfer transaction ethrex_10_transfers.bin - sha256: d04cfed35bd16c8248ab8ebf2f3ca2ff01c08269271b17e5d5db3b1f22ea03ad + sha256: 4d862e8537284729ff11c7bcf91c971e562dd6bbce2a1e181ba5bf48cb6b65cf contents: stateless ethrex block with ten plain ETH transfer transactions ethrex_bench_4.bin - sha256: bb26e6d6595f537c2fd57bfd59da6abe82241bd92690a21c2225da94d5cf507a + sha256: 03ed0d175622af6ef9a981d7652ba7c86630b9473f49cae17edf649724b704e1 contents: stateless ethrex block with four plain ETH transfers, `distinct` mode (N senders -> N recipients); read by the recursion profile target ``` diff --git a/executor/tests/ethrex_10_transfers.bin b/executor/tests/ethrex_10_transfers.bin index dd9303cfe4167942b6d264c42058a2ddc6c3c544..201351189e5462b12704ee2e5084ad44ab349682 100644 GIT binary patch delta 70 zcmZ3Kxg~Rh2KU4OPKonh{{QD-G`~N?XVqlblFeWeLmfRf4ZKlq6Wb*_gX~xM5 WM7SrfHQvqi?f2x15(bkExHtggr5hsv delta 74 zcmdmzxhQjk2DkK$FaQ5@F)%Rb{QdtQNHH)fLm9@!lNEV{H^*~ZsxzM5Jkv;;ak2py X$7CI2&dKMD*D(G6J(*F|03-weDz6$@ diff --git a/executor/tests/ethrex_bench_4.bin b/executor/tests/ethrex_bench_4.bin index 6b716bfc9002715b7c010a4d17072122742f5b75..23c2453ee946e46c1b8ab3d8b9fca79d03b11d11 100644 GIT binary patch delta 74 zcmZ2icc5;A3fsg0PKnDu|Nm!UU|>-G`~N?XVqlblFeZOsliVE2cFc^?WV4K|G~?tC ahTN0&SlK4q+ihj~_IvU}4TH%#tQ-Jb7aT1B delta 61 zcmX?5x2|r33Y*ZKpa1`}FfcIa{QdtQNHH)fZ?khSCNE@`+|0&oC&PGjv#Y!`-G`~N?XVqlblFec}*NN&E&Y^Kh5WHXnxG~?t4 ZQrwgKwAm-?>1<*8_It9Tq!EzI1^_i%8+QNz delta 61 zcmZ1%H79DrB4(kuKmY$TGB7ac{QdtQNHH)fZ{Euss?K diff --git a/scripts/set_ethrex_rev.sh b/scripts/set_ethrex_rev.sh new file mode 100755 index 000000000..0ae03673d --- /dev/null +++ b/scripts/set_ethrex_rev.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Re-pin every ethrex git dependency in this repo to one rev, atomically. +# +# The guest and the host-side tooling exchange ethrex types (the SSZ stateless +# input the converter writes and the guest decodes), so a rev that differs +# between them is not a version skew that fails to build -- it is a fixture the +# guest silently decodes as the all-zero default. Keeping every pin on one rev +# is what makes that impossible, which is why this rewrites all of them together +# rather than leaving them to be bumped by hand. +# +# Usage: +# scripts/set_ethrex_rev.sh <40-char-sha> re-pin everything +# scripts/set_ethrex_rev.sh --guest-only <40-char-sha> re-pin only the guest graph +# scripts/set_ethrex_rev.sh --show print the current pin(s) +# +# `--guest-only` moves the crates the guest ELF is built from and leaves the +# host-side fixture tooling where it is. That split is what lets a benchmark vary +# the guest across two revs while holding the fixture constant: the fixture is +# just bytes, its wire format does not change within a PR, and regenerating it per +# rev would instead drag the whole ethrex HOST api (which does drift) into the +# measurement. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# Every manifest carrying an ethrex git dep. Kept explicit rather than globbed so +# a new one has to be added deliberately -- a manifest silently left behind is +# exactly the skew described above. +# The guest ELF's own graph. `crypto/ethrex-crypto` is a path dep of BOTH the +# guest and the converter, so moving it moves what the converter links too -- +# which is why a `--guest-only` run must come after the fixture already exists. +GUEST_MANIFESTS=( + executor/programs/rust/ethrex/Cargo.toml + crypto/ethrex-crypto/Cargo.toml +) + +# Host-side tooling: builds fixtures, never lands in the ELF. +TOOLING_MANIFESTS=( + tooling/ethrex-tests/Cargo.toml + tooling/ethrex-block-converter/Cargo.toml + tooling/ethrex-fixtures/Cargo.toml +) + +MANIFESTS=("${GUEST_MANIFESTS[@]}" "${TOOLING_MANIFESTS[@]}") + +current_revs() { + grep -ho 'rev = "[0-9a-f]\{40\}"' "${MANIFESTS[@]}" | sort -u | sed 's/rev = //;s/"//g' +} + +if [[ "${1:-}" == "--show" || $# -eq 0 ]]; then + # No `mapfile`: macOS ships bash 3.2, where it does not exist. + revs="$(current_revs)" + if [[ "$(printf '%s\n' "$revs" | wc -l | tr -d ' ')" == "1" ]]; then + echo "$revs" + else + # Expected mid-benchmark (a `--guest-only` re-pin is exactly this state), so + # report it rather than failing. + echo "MIXED:" >&2 + printf ' %s\n' $revs >&2 + exit 1 + fi + exit 0 +fi + +TARGETS=("${MANIFESTS[@]}") +SCOPE="all manifests" +if [[ "${1:-}" == "--guest-only" ]]; then + shift + TARGETS=("${GUEST_MANIFESTS[@]}") + SCOPE="the guest graph only" +fi + +NEW_REV="${1:-}" +if [[ ! "$NEW_REV" =~ ^[0-9a-f]{40}$ ]]; then + echo "error: expected a 40-character commit sha, got '$NEW_REV'" >&2 + exit 2 +fi + +for manifest in "${TARGETS[@]}"; do + [[ -f "$manifest" ]] || { echo "error: missing $manifest" >&2; exit 1; } + # Only rewrite revs on ethrex deps: the same file may pin other git deps. + perl -pi -e 's{(github\.com/lambdaclass/ethrex\.git", rev = ")[0-9a-f]{40}(")}{${1}'"$NEW_REV"'${2}}g' "$manifest" +done + +echo "re-pinned $SCOPE to $NEW_REV:" +printf ' %s\n' "${TARGETS[@]}" diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index e68dfc38c..5c216acbb 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -305,19 +305,6 @@ 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" @@ -877,18 +864,21 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crossbeam", "ethrex-common", "ethrex-crypto", + "ethrex-guest-program", "ethrex-metrics", "ethrex-rlp", "ethrex-storage", "ethrex-trie", "ethrex-vm", + "libssz", + "libssz-merkle", "rayon", "rustc-hash", "thiserror 2.0.19", @@ -899,8 +889,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crc32fast", @@ -914,6 +904,10 @@ dependencies = [ "indexmap 2.14.0", "lazy_static", "libc", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "lru", "once_cell", "rkyv", @@ -927,8 +921,8 @@ dependencies = [ [[package]] name = "ethrex-config" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ethrex-common", "ethrex-p2p", @@ -939,8 +933,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ark-bn254", "ark-ec", @@ -960,10 +954,9 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ - "bls12_381", "bytes", "ethereum-types", "ethrex-common", @@ -971,10 +964,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-rlp", "ethrex-vm", - "ff", "hex", - "k256", - "lambda-vm-syscalls", "rkyv", "serde", "serde_with", @@ -983,8 +973,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -993,7 +983,6 @@ dependencies = [ "k256", "lambdaworks-crypto", "rkyv", - "secp256k1", "serde", "serde_with", "thiserror 2.0.19", @@ -1002,14 +991,15 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", "ethrex-common", "ethrex-crypto", "ethrex-rlp", + "libssz", "malachite", "rustc-hash", "serde", @@ -1019,8 +1009,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ethrex-common", "serde", @@ -1031,8 +1021,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "aes", "aes-gcm", @@ -1073,8 +1063,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -1083,8 +1073,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -1105,8 +1095,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -1125,8 +1115,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", @@ -1159,7 +1149,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -1700,6 +1689,47 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libssz" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "libssz-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libssz-merkle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +dependencies = [ + "libssz", + "sha2", +] + +[[package]] +name = "libssz-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +dependencies = [ + "libssz", + "libssz-merkle", + "smallvec", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1926,15 +1956,6 @@ dependencies = [ "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" diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 87550cd8a..28f8d7e1d 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -9,13 +9,13 @@ 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 = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-common", default-features = false } -ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-config", default-features = false } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-common", default-features = false } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-config", default-features = false } # Only for `NativeCrypto`, the host `Crypto` impl that `into_execution_witness` # now takes. Nothing re-exports it, and this crate's own guest-crypto impl is a # dev-dependency, so the bin build needs its own line. Already in the graph via # `ethrex-config` → `ethrex-p2p`, so this adds no compilation. -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-crypto", default-features = false } # 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 @@ -26,7 +26,7 @@ ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f65 # `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 = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program", default-features = false } # Exact pin: the fixture writer and the guest/executor readers must agree on the # rkyv layout. Keep this in sync with the guest diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index 545c7cc6e..87bdfe51d 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -230,7 +230,7 @@ mod tests { .map(|b| format!("{b:02x}")) .collect(); assert_eq!( - digest, "6ffc80b25ec9ed3c203c35bdb8a4a05aff08d98b1a9c71648447bc199a552dc9", + digest, "8b3fb1294a5cd7296452d4a0422b0623c2d243e84bc8a51d3f15bc3899354553", "fixture bytes changed — regenerate it and update the README checksum", ); } diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index 1702880a9..03e77ca4a 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -975,18 +975,21 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crossbeam", "ethrex-common", "ethrex-crypto", + "ethrex-guest-program", "ethrex-metrics", "ethrex-rlp", "ethrex-storage", "ethrex-trie", "ethrex-vm", + "libssz", + "libssz-merkle", "rayon", "rustc-hash", "thiserror 2.0.18", @@ -997,8 +1000,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crc32fast", @@ -1012,6 +1015,10 @@ dependencies = [ "indexmap 2.14.0", "lazy_static", "libc", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "lru", "once_cell", "rayon", @@ -1027,8 +1034,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ark-bn254", "ark-ec", @@ -1066,8 +1073,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -1085,8 +1092,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -1104,8 +1111,8 @@ dependencies = [ [[package]] name = "ethrex-l2-rpc" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "axum", "bytes", @@ -1136,14 +1143,15 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", "ethrex-common", "ethrex-crypto", "ethrex-rlp", + "libssz", "malachite", "rayon", "rustc-hash", @@ -1154,8 +1162,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "axum", "ethrex-common", @@ -1170,8 +1178,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "aes", "aes-gcm", @@ -1214,8 +1222,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -1224,8 +1232,8 @@ dependencies = [ [[package]] name = "ethrex-rpc" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "axum", "axum-extra", @@ -1264,8 +1272,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -1286,8 +1294,8 @@ dependencies = [ [[package]] name = "ethrex-storage-rollup" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "async-trait", "bincode", @@ -1302,8 +1310,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -1322,8 +1330,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", @@ -2212,6 +2220,47 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libssz" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "libssz-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libssz-merkle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +dependencies = [ + "libssz", + "sha2", +] + +[[package]] +name = "libssz-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +dependencies = [ + "libssz", + "libssz-merkle", + "smallvec", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index 3320ba25b..daeda8b15 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -9,11 +9,11 @@ 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-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-blockchain" } -ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-storage" } -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-common" } -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program" } -ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-l2-rpc" } +ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-blockchain" } +ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-storage" } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-common" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program" } +ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-l2-rpc" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # Exact pin: the fixture writer and the guest/executor readers must agree on the diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 84712135c..0a86228f1 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -693,8 +693,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "crc32fast", @@ -708,6 +708,10 @@ dependencies = [ "indexmap 2.14.0", "lazy_static", "libc", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "lru", "once_cell", "rayon", @@ -723,8 +727,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "ark-bn254", "ark-ec", @@ -744,8 +748,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -763,8 +767,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -782,14 +786,15 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", "ethrex-common", "ethrex-crypto", "ethrex-rlp", + "libssz", "malachite", "rayon", "rustc-hash", @@ -800,8 +805,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "ethereum-types", @@ -819,8 +824,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "anyhow", "bytes", @@ -839,8 +844,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "23.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" dependencies = [ "bytes", "derive_more", @@ -1275,6 +1280,47 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libssz" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +dependencies = [ + "smallvec", +] + +[[package]] +name = "libssz-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libssz-merkle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +dependencies = [ + "libssz", + "sha2", +] + +[[package]] +name = "libssz-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +dependencies = [ + "libssz", + "libssz-merkle", + "smallvec", +] + [[package]] name = "log" version = "0.4.33" @@ -1928,6 +1974,12 @@ 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 = "spin" version = "0.9.9" diff --git a/tooling/ethrex-tests/Cargo.toml b/tooling/ethrex-tests/Cargo.toml index 5ff8ef39f..3c89ea8f7 100644 --- a/tooling/ethrex-tests/Cargo.toml +++ b/tooling/ethrex-tests/Cargo.toml @@ -12,7 +12,7 @@ edition = "2024" executor = { path = "../../executor" } # Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch) # so the native reference reads the same ProgramInput rkyv layout. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-guest-program" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program" } # Exact pin: the fixture writer and the guest/executor readers must agree on the # rkyv layout. Keep this in sync with tooling/ethrex-fixtures and # executor/programs/rust/ethrex/Cargo.toml. From fdbd91c61ed159092d5c09c1ece9ecad2f3989b6 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 26 Aug 2026 17:25:49 -0300 Subject: [PATCH 06/46] rm file --- BUMP_797df554_RESULTS.md | 130 --------------------------------------- 1 file changed, 130 deletions(-) delete mode 100644 BUMP_797df554_RESULTS.md diff --git a/BUMP_797df554_RESULTS.md b/BUMP_797df554_RESULTS.md deleted file mode 100644 index 21a6b1525..000000000 --- a/BUMP_797df554_RESULTS.md +++ /dev/null @@ -1,130 +0,0 @@ -# Bumping the ethrex pin to `797df554` - -Moves the guest from `4f658c2b` (2026-07-31, on the unmerged -`feat/lambdavm-prover-backend` branch) to `797df554` (2026-08-07, on ethrex -`main`). - -## Results - -Real mainnet block 25368371 — the fixture `bench_abba.sh`, `bench_verify.sh`, -`perf_diff.sh` and `benchmark-pr.yml` all resolve through -`make print-real-block-fixture`. - -| | before (`4f658c2b`) | after (`797df554`) | delta | -|---|---|---|---| -| **Proving time** (median of 3) | **120.715 s** | **110.259 s** | **−8.66%** | -| run spread | 0.681 s (0.56%) | 0.821 s (0.74%) | | -| Executed cycles | 34,241,608 | 30,498,818 | −10.93% | -| **Epochs** (2^22) | **9** | **8** | −1 | -| Peak heap | 47.3 GB | 46.2 GB | −2.4% | - -10.46 s off every proof, against a run spread below 0.8% on both sides — the -delta is roughly twelve times the noise. - -Against `main`'s pin (`156cb8d6`, 2026-05-22) the guest goes from **39,563,400** -to **30,498,818** cycles, **−22.9%**. - -### Time does not track cycles - -−10.93% of cycles buys −8.66% of proving time, and that gap is structural rather -than measurement error: proving cost also carries per-epoch fixed work and table -rows that do not shrink with the instruction count. Quoting a cycle delta as if it -were a time delta will overpromise. - -The epoch count is the clearest instance. At 2^22 cycles per epoch, 34.2M needed 9 -and 30.5M fits in 8, and that single step accounts for much of the 10 s. It also -means the next increment of cycle savings returns less until it drops the ninth -epoch — the benefit arrives in steps, not smoothly. - -## What changed - -**The pin**, in all 11 places (`scripts/set_ethrex_rev.sh` moves them together, so -the guest and the fixture tooling cannot drift apart — a mismatch there does not -fail to build, it produces a fixture the guest silently misreads). - -**The `lambdavm` feature is gone**, and not replaced. `ethrex-guest-program` -carries one feature per zkVM and none of them selects a backend: each is only a -list of optional crypto dependencies, and none gates any method of the `Crypto` -trait (its gates are `secp256k1`, `c-kzg`, `blst`, `std`). Measured against the -sibling feature that activates the widest dependency set, ethrex's own precompile -stress fixtures come out identical to the cycle — `stress_modexp_150M` -6,373,285,966 and `stress_alt_bn128_150M` 22,986,061,145 both ways — with the real -block at 30,498,818 vs 30,501,620 and a 2,928-byte smaller ELF. - -Requiring `lambdavm` is what tied the guest to the backend branch, since that is -the only place it exists. What makes this guest LambdaVM never travelled through -it: `lambda-vm-syscalls`, `lambda-vm-ethrex-crypto` and the -`riscv64im-lambda-vm-elf` target are direct, and every run above reports 116 ECSM -and 10,659 keccak precompile calls — the injected crypto is live on both sides. - -**Fixtures regenerated.** The rkyv `ProgramInput` layout moved with the rev, so -every committed `.bin` from the old pin fails to deserialize. All four synthetic -fixtures plus the real block were rebuilt; the converter's reproducibility digest -was updated, which is what that test asks for on a legitimate rev bump. - -| fixture | cycles | ECSM | -|---|---|---| -| `ethrex_empty_block` | 431,162 | 0 | -| `ethrex_simple_tx` | 653,263 | 4 | -| `ethrex_bench_4` | 1,047,001 | 16 | -| `ethrex_10_transfers` | 1,286,761 | 40 | -| real block 25368371 | 30,498,818 | 116 | - -## Why `797df554` and not something newer - -`b5271885` (2026-08-14) rewrites the guest entry point from `ProgramInput` / -`execution_program` to `run_stateless_guest` taking schema-prefixed SSZ -`statelessInputBytes`. Past that commit this is no longer a pin bump — it needs a -new guest shim and a new fixture format. `797df554` is the last point where the -bump is only the rev. - -Within that window it is also the right end of it: between 2026-08-07 and the -boundary there are 14 commits and **no `perf(levm)`**. The two that matter — -`perf(levm): monomorphize the dispatch loop on the validation observer` (#7105) -and `perf(l1): give branch-node hashing a monomorphic RLP encoder` (#7104) — are -already in. - -Release tags do not help here. `v24.0.0`'s commit predates both, and it is -*diverged* from `main` (4 commits of its own, 66 behind) because ethrex cuts -releases from a side branch rather than from the tip — so a tag can lack work that -is already merged. `v25.0.0` is past the API boundary. - -## Method - -`vm-benchmarks-1`, 96 cores / 125 GB, rustc 1.94.0. The real-block step of -`.github/workflows/benchmark-pr.yml` verbatim: - -``` -cli prove $ELF --private-input $REAL_INPUT \ - --continuations --epoch-size-log2 22 -o /tmp/real_proof.bin --time -``` - -3 runs per side, reported as median plus spread. - -Both sides started on an idle box (load 0.61 and 1.99). The baseline was held back -until load fell below 3 after the first side finished — starting it hot would have -inflated the baseline and flattered the bump. - -Each side used **its own canonical fixture**: the baseline fetched the published -`_4f658c2b` release asset, the bumped side used the newly generated `_797df554` -one. They cannot share a file — the rkyv layout differs — and each side's own -asset is what CI would fetch, which is the honest comparison rather than two -ad-hoc conversions. - -Cycle counts are deterministic: the bumped side reported 30,498,818 identically on -a laptop and on the bench box. - -Raw logs in `bench_ethrex_bump_20260826/`. - -## Before merging - -`ethrex_mainnet_25368371_797df554.bin` (in the repo root) must be uploaded to the -`bench-fixtures-v1` release of **yetanotherco/lambda_vm**. The Makefile already -points at it with its sha256; until the asset exists, `make -ethrex-real-block-fixture` works locally (the file is in place and verifies) but -CI cannot fetch it. - -This also stops being a routine bump: it leaves `feat/lambdavm-prover-backend` and -consumes ethrex `main` as a library. That branch is 69 commits behind `main` and -has not moved since 2026-08-04, so waiting for it to merge was itself costing the -−22.9% above. The PR's existing approval predates this change. From fecc3f3dedfcd3cfa0b4a0d51d57240d49771ada Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Tue, 8 Sep 2026 17:30:11 -0300 Subject: [PATCH 07/46] Move the ethrex pin from 797df554 (23.0.0) to 2cb18b0b (25.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since execution-specs #3278 the guest takes the spec's schema-prefixed SSZ statelessInputBytes instead of an rkyv ProgramInput, with the fork coming from the schema id — ethrex 25 implements one, 0x1501 (Amsterdam) — so the guest, the converter, the fixture generator and the host-reference tests move to that format and the four synthetic fixtures are regenerated. The benchmark fixture becomes generated rather than fetched: mainnet has no Amsterdam fork, so no hosted artifact for that block can be valid, and the old one now runs 496 cycles with successful_validation = 0 instead of failing. real_block rebuilds mainnet 25368371 as an Amsterdam block from its replay cache, and bench_abba.sh refuses to measure a workload the guest rejected. --- .github/workflows/benchmark-pr.yml | 24 +- .github/workflows/ethrex-block-converter.yml | 16 +- Makefile | 65 +- crypto/ethrex-crypto/Cargo.lock | 4 +- crypto/ethrex-crypto/Cargo.toml | 2 +- crypto/ethrex-crypto/src/lib.rs | 3 +- executor/programs/rust/ethrex/Cargo.lock | 53 +- executor/programs/rust/ethrex/Cargo.toml | 6 +- executor/programs/rust/ethrex/src/main.rs | 21 +- executor/tests/README.md | 76 +-- executor/tests/ethrex_10_transfers.bin | Bin 13492 -> 9443 bytes executor/tests/ethrex_bench_4.bin | Bin 16192 -> 10056 bytes executor/tests/ethrex_empty_block.bin | Bin 8544 -> 6281 bytes executor/tests/ethrex_simple_tx.bin | Bin 11566 -> 7441 bytes scripts/bench_abba.sh | 35 +- tooling/ethrex-block-converter/Cargo.lock | 78 +-- tooling/ethrex-block-converter/Cargo.toml | 34 +- tooling/ethrex-block-converter/README.md | 577 ++---------------- tooling/ethrex-block-converter/src/main.rs | 383 +++++++----- tooling/ethrex-fixtures/Cargo.lock | 88 +-- tooling/ethrex-fixtures/Cargo.toml | 24 +- tooling/ethrex-fixtures/README.md | 123 ++-- tooling/ethrex-fixtures/genesis.json | 12 + tooling/ethrex-fixtures/src/bin/real_block.rs | 512 ++++++++++++++++ tooling/ethrex-fixtures/src/main.rs | 177 +++++- tooling/ethrex-tests/Cargo.lock | 58 +- tooling/ethrex-tests/Cargo.toml | 16 +- .../tests/eest_amsterdam_probe.rs | 81 +++ tooling/ethrex-tests/tests/ethrex.rs | 148 +---- 29 files changed, 1446 insertions(+), 1170 deletions(-) create mode 100644 tooling/ethrex-fixtures/src/bin/real_block.rs create mode 100644 tooling/ethrex-tests/tests/eest_amsterdam_probe.rs diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index b9da23925..934dd1996 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -76,9 +76,9 @@ env: # 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 - # 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. + # REAL_INPUT at run time. The .bin is gitignored and BUILT from the block's replay + # cache (see "Build ethrex real-block fixture"); while that cache's 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 @@ -240,8 +240,8 @@ jobs: # the growth sweep still runs and feeds the baseline artifact (no comment is # posted on those events — the Comment step is gated to comment triggers); # on a plain /bench nothing is left to measure, and the footer says so. - 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." + if [ "$RUN_REAL" = "true" ] && [ -z "$(make -s print-real-block-cache-url)" ]; then + echo "::warning::Real-block benchmark skipped: ETHREX_REAL_BLOCK_CACHE_URL is unset in the Makefile." RUN_REAL=false fi echo "run_real=$RUN_REAL" >> "$GITHUB_OUTPUT" @@ -296,14 +296,16 @@ jobs: echo "Using $RUNS iterations, TABLE_PARALLELISM=default" fi - - name: Fetch ethrex real-block fixture + - name: Build ethrex real-block fixture if: steps.config.outputs.run_real == 'true' run: | - # ~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"). + # Gitignored and never in the checkout, like the synthetic fixtures above. + # Built, not downloaded: ethrex 25's guest decodes only the Amsterdam schema, + # so a hosted artifact for this pre-Amsterdam block cannot be valid — it would + # run ~500 cycles and commit `successful_validation = 0` without failing. The + # generator rebuilds the block's real transactions on its real pre-state and + # validates the result through the guest before writing. The step is gated on + # the cache 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. diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index 53c640e9b..e8260e129 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -83,15 +83,17 @@ jobs: workspaces: | tooling/ethrex-tests -> target - # Fetch-and-verify, not build: no converter, no ethrex-replay cache, no rev pin. - # 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 + # Built from the block's replay cache, not downloaded: ethrex 25's guest decodes + # only the Amsterdam schema, so no hosted artifact for a pre-Amsterdam block can + # be valid. The generator validates the block through the guest before writing, + # which is what the screen below then re-checks against the ELF. The guard covers + # an unset cache URL, where failing the job would block PRs on an upload nobody + # in the PR can perform. + - name: Build 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." + if [ -z "$(make -s print-real-block-cache-url)" ]; then + echo "::warning::ETHREX_REAL_BLOCK_CACHE_URL is unset — skipping the real-block usability screen. Set it in the Makefile once the cache is hosted." echo "present=false" >> "$GITHUB_OUTPUT" exit 0 fi diff --git a/Makefile b/Makefile index 1f178b3c1..ced7743f2 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ 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 ethrex-real-block-converter-cache print-real-block-fixture \ -print-real-block-fixture-url \ +print-real-block-cache-url \ test-ethrex-real-block-converter regen-real-block-fixture UNAME := $(shell uname) @@ -280,7 +280,7 @@ test-rust: compile-programs-rust # from tooling/ethrex-fixtures. Two artifacts, both gitignored and both FETCHED # rather than built: # -# the fixture the rkyv ProgramInput the benchmarks prove (~1 MB) +# the fixture the schema-prefixed SSZ input the benchmarks prove # the cache the ethrex-replay JSON it was converted from (~2 MB), read only # by `regen-real-block-fixture`. The converter's TESTS read a # different, upstream-pinned cache — see below. @@ -307,14 +307,22 @@ test-rust: compile-programs-rust # measured cost. ETHREX_REAL_BLOCK_NETWORK := mainnet ETHREX_REAL_BLOCK := 25368371 -# The asset name carries the ethrex rev because the bytes are a function of it: the -# archived ProgramInput layout moves with the pin, so one block has one fixture per rev. -# Uploading under a new name rather than replacing the old one keeps `main` — which still -# expects the pre-bump sha256 — fetching its own artifact while this branch is open. -ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371_797df554.bin -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 573004e62e3680a00d3cdbae19dc4897e2ec60d6ec0c1d05d9ef118cb8aef17f -# 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). +# The fixture is GENERATED from the cache below, not fetched. ethrex 25's guest +# decodes only the Amsterdam schema (`0x1501`) and mainnet has no Amsterdam fork, +# so no hosted artifact for this block can be valid: the release's rkyv one now +# runs 496 cycles and commits `successful_validation = 0` instead of failing. +# `tooling/ethrex-fixtures --bin real_block` rebuilds the block's real +# transactions and real pre-state as an Amsterdam block and validates the result +# through the guest before writing it, so a stale or wrong-fork fixture cannot +# reach a benchmark. Nothing to publish, and it works offline once the cache is +# there. Read the caveat in tooling/ethrex-fixtures/README.md before quoting +# numbers: Amsterdam's gas model (EIP-8037 state gas, cold access 2600 -> 3000) +# makes 10 of this block's Osaka-era transactions run out of gas. +ETHREX_REAL_BLOCK_FIXTURE_URL := +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := +# The block's source cache: an ethrex-replay dump, fork-independent, still the one +# hosted in bench-fixtures-v1. Only the fixture rebuild reads it; converter 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 @@ -375,8 +383,16 @@ define ensure_verified 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) +# A file rule, unlike the fetched artifacts above: those are phony so their digest +# is re-checked on every invocation, which is how a stale download is caught. This +# one is BUILT from the cache (whose digest IS re-checked) by a deterministic +# generator that validates the block through the guest before writing, so the file +# existing is proof enough and rebuilding it on every benchmark would just add a +# cargo build to the critical path. +ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) + +$(ETHREX_REAL_BLOCK_FIXTURE): ethrex-real-block-cache + $(MAKE) regen-real-block-fixture 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) @@ -389,10 +405,11 @@ ethrex-real-block-cache: 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) +# Lets CI ask "can the fixture be produced?" without parsing the Makefile. The +# fixture itself is generated, so what has to be hosted is its source cache; +# prints nothing while that URL is unset, which is the condition callers branch on. +print-real-block-cache-url: + @echo $(ETHREX_REAL_BLOCK_CACHE_URL) # ===== Real-block converter (regeneration tool, off the build path) ===== # @@ -402,7 +419,7 @@ print-real-block-fixture-url: # # 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 +# exercise is the CONVERSION — cache JSON in, correctly serialized SSZ 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. @@ -439,16 +456,16 @@ ethrex-real-block-converter-cache: $(ETHREX_CONVERTER_CACHE) test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE) 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. +# The BENCHMARK fixture (not the converter's test block): rebuilt from that block's +# own replay cache. Deterministic, so re-running it is how the fixture is replaced — +# there is no artifact to upload. regen-real-block-fixture: ethrex-real-block-cache - cd tooling/ethrex-block-converter && \ - cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) + cd tooling/ethrex-fixtures && \ + cargo run --release --bin real_block -- \ + ../../$(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). +# workspace (the ethrex host dependency tree is isolated in its own Cargo.lock). # Needs the real-block fixture, so it needs the fixture URL to be set. This is a # local convenience target: no workflow invokes it. The PR gate spells out the # `-offline` variant below inline (pr_main.yaml), and ethrex-block-converter.yml's diff --git a/crypto/ethrex-crypto/Cargo.lock b/crypto/ethrex-crypto/Cargo.lock index 0cdd80692..67a36c419 100644 --- a/crypto/ethrex-crypto/Cargo.lock +++ b/crypto/ethrex-crypto/Cargo.lock @@ -342,8 +342,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "22.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ark-bn254", "ark-ec", diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml index 09d53ac3b..f5315df65 100644 --- a/crypto/ethrex-crypto/Cargo.toml +++ b/crypto/ethrex-crypto/Cargo.toml @@ -18,7 +18,7 @@ license = "MIT OR Apache-2.0" # Defines the `Crypto` trait, `CryptoError`, and `keccak::keccak_hash`. Same rev # + `default-features = false` as the guest's ethrex-crypto, so feature # unification adds nothing to the guest build (no C secp256k1 / malachite / kzg). -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-crypto", default-features = false } # Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 # (a version split would make `FieldElement`/`Scalar` incompatible types). # `expose-field` is required by the x-only reconstruction. diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index ec36b0831..6625a99ee 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -2,7 +2,7 @@ //! //! Implements ethrex's `Crypto` trait with LambdaVM precompile acceleration and //! is injected into the ethrex guest (`Arc::new(LambdaVmEcsmCrypto)` → -//! `execution_program`). Living in the lambda_vm repo (not in ethrex) means +//! `run_stateless_guest`). Living in the lambda_vm repo (not in ethrex) means //! accelerator changes don't require an ethrex PR. //! //! Accelerated today: @@ -16,6 +16,7 @@ //! Every other `Crypto` method inherits the trait default (vetted pure-Rust //! crates: `ark-bn254`, `bls12_381`, `p256`, `sha2`, `ripemd`, …). +#[cfg(not(target_arch = "riscv64"))] use ethrex_crypto::keccak::keccak_hash; use ethrex_crypto::{Crypto, CryptoError}; use k256::elliptic_curve::group::prime::PrimeCurveAffine; diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index 63705e473..14f929569 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -663,13 +663,12 @@ dependencies = [ "ethrex-guest-program", "lambda-vm-ethrex-crypto", "lambda-vm-syscalls", - "rkyv", ] [[package]] name = "ethrex-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crc32fast", @@ -700,8 +699,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ark-bn254", "ark-ec", @@ -720,8 +719,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -731,6 +730,10 @@ dependencies = [ "ethrex-rlp", "ethrex-vm", "hex", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "rkyv", "serde", "serde_with", @@ -739,8 +742,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -757,8 +760,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -775,8 +778,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -785,8 +788,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -805,8 +808,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -1247,18 +1250,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssz" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +checksum = "b4bdd6d63ed811ae164966de20810be780e07de784a4834ccfe6be90480c369e" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +checksum = "aeee1b9ac9200429f7e9830492765445989ea61c3fb9028ad5a96e1dd5f5e913" dependencies = [ "proc-macro2", "quote", @@ -1267,9 +1270,9 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +checksum = "863eca32d1a43e5ec41106a515552efa8307768d37c68d26b7f21ff13cfee1a7" dependencies = [ "libssz", "sha2", @@ -1277,9 +1280,9 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +checksum = "d4231ac301726840a3fe111f11bd4619d3c97ed155cb94c88dbf92b70e04e017" dependencies = [ "libssz", "libssz-merkle", diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 1d2ef0f4a..ebef0bffe 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -54,11 +54,7 @@ lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # # KZG is not linked, so the point-evaluation precompile (0x0a) is unsupported — # see src/main.rs. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program", default-features = false } -# Exact pin: must match the fixture writer (tooling/ethrex-fixtures) and the -# executor test reader so the rkyv ProgramInput layout stays consistent. -rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } - +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program", default-features = false } # `lambda-vm-syscalls` can still reach the graph as a git dep pinned to a commit # of THIS repo, which upstream bumps on its own schedule. Override it # with the working-tree copy so the guest always links the same syscalls as the rest diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index 8154978cf..8ea1c5093 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -1,26 +1,11 @@ use std::sync::Arc; -use ethrex_guest_program::l1::{ProgramInput, execution_program}; +use ethrex_guest_program::l1::run_stateless_guest; use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; -use rkyv::rancor::Error; pub fn main() { - // Zero-copy private input: borrow the memory-mapped input region in place - // (the host pre-loads it before execution) so rkyv deserializes straight - // out of it. `get_private_input()` is this same slice plus a `to_vec()` — - // a full extra copy and one large allocation (~50k cycles on a 20-tx - // block). let input = lambda_vm_syscalls::syscalls::get_private_input_slice(); - let input = rkyv::from_bytes::(input).unwrap(); - // LambdaVM crypto provider, defined in the lambda_vm repo and injected here - // (so crypto changes don't require an ethrex PR — see `crypto/ethrex-crypto`). - // It accelerates trait-routed `keccak256` (via the keccak_permute precompile) - // and `secp256k1_ecrecover` (via the ECSM precompile); everything else uses - // ethrex's pure-Rust trait defaults. ethrex's trie/RLP keccak that goes - // through the free `keccak_hash` fn is still software, and KZG (0x0a) is - // unsupported under the `lambdavm` feature (blob txs execute; a point-eval - // precompile call reverts). let crypto = Arc::new(LambdaVmEcsmCrypto); - let output = execution_program(input, crypto).unwrap(); - lambda_vm_syscalls::syscalls::commit(&output.encode()); + let output = run_stateless_guest(input, crypto); + lambda_vm_syscalls::syscalls::commit(&output); } diff --git a/executor/tests/README.md b/executor/tests/README.md index 6ecbc7212..36b8fb70a 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -1,79 +1,47 @@ # Executor Test Fixtures -## Ethrex private inputs +The `ethrex_*.bin` files are schema-prefixed SSZ stateless inputs consumed by +`ethrex_guest_program::l1::run_stateless_guest`. The first two bytes are the +Amsterdam schema ID (`0x1501`); the body contains the payload, public keys, and +execution witness. -The `ethrex_*.bin` files are rkyv-serialized `ethrex_guest_program::l1::ProgramInput` -values consumed by the ethrex guest (`executor/programs/rust/ethrex`). - -The native-reference tests live in `tooling/ethrex-tests` (a detached -workspace: ethrex pins rkyv `unaligned`, which must not feature-unify with the -main workspace's aligned proof format). - -The ethrex guest, the native test reference, and the fixture generator are all -pinned to the same ethrex revision (the open LambdaVM-backend PR branch, until it -merges to `main`): +The guest, native reference tests, and fixture generator all use this ethrex +commit: ```text https://github.com/lambdaclass/ethrex.git -4f658c2b3d10e3f21d35ce546870f55ca3f940fc +2cb18b0b95b27a2555d3debffdebc43c9685d6e3 ``` -### Generation - -These blobs are generated reproducibly by the in-repo tool `tooling/ethrex-fixtures` -(in-memory, offline — no RPC). It builds a synthetic block with N signed ETH -transfers from a funded genesis account and serializes the resulting -`ProgramInput`: +Generate the committed fixtures with: ```bash -cd tooling/ethrex-fixtures -cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin # empty block -cargo run --release -- 1 ../../executor/tests/ethrex_simple_tx.bin # 1 transfer -cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin # 10 transfers -cargo run --release -- 4 ../../executor/tests/ethrex_bench_4.bin distinct # recursion profile +make regen-ethrex-fixtures ``` -`ethrex_bench_4.bin` is the odd one out: `distinct` mode, and it is read by the -recursion profile target rather than the executor tests (see the Makefile's -`recursion-profile-block-input`). It is committed like the rest, so it is -regenerated and checksummed with them — a rev bump makes every one of these -undecodable, not just the three the executor reads. - -To regenerate after an ethrex rev bump, update the `rev` in -`tooling/ethrex-fixtures/Cargo.toml` (and the guest's), then run -`make regen-ethrex-fixtures` from the repo root. The target rebuilds the -committed fixtures and refreshes the checksums below. +The generator enables Amsterdam in its synthetic genesis and includes the two +EIP-8282 request predeploys required by ethrex 25. Known fixtures: ```text ethrex_empty_block.bin - sha256: 8d6f6061c71c23fad1d5dee26242d631efe0bff8d7f49422c2ba4cde9d4be919 - contents: stateless ethrex empty block ProgramInput (0 transactions) + sha256: d914d36e673dc0e24bc4e105f3037e78305e63f6121e1937058dcc704fabbb8e + contents: stateless ethrex empty block (0 transactions) ethrex_simple_tx.bin - sha256: c40bce364f22758ab7fa6fe8b45ce4c305dee5add4536ef6dca0e74e410e2729 - contents: stateless ethrex block with one plain ETH transfer transaction + sha256: 4dd4ab89d904981844f28b093fde0ed18ffa4d61273482eb8592d41db6a38e7d + contents: stateless ethrex block with one plain ETH transfer ethrex_10_transfers.bin - sha256: 4d862e8537284729ff11c7bcf91c971e562dd6bbce2a1e181ba5bf48cb6b65cf - contents: stateless ethrex block with ten plain ETH transfer transactions + sha256: e86c5fc80b8b603c4a58fd6ab6ce5bbb40d378c67c8b65f4d89a15b01f69fc6f + contents: stateless ethrex block with ten plain ETH transfers ethrex_bench_4.bin - sha256: 03ed0d175622af6ef9a981d7652ba7c86630b9473f49cae17edf649724b704e1 - contents: stateless ethrex block with four plain ETH transfers, `distinct` mode - (N senders -> N recipients); read by the recursion profile target + sha256: dbfe0d808ff9476ef70bfd4459b82330a2dc038bdfdf2808447ed04012556386 + contents: stateless ethrex block with four distinct plain ETH transfers ``` -## 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` downloads -`ethrex_mainnet_25368371_4f658c2b.bin` (1,110,165 B) from the `bench-fixtures-v1` release -and verifies it against `ETHREX_REAL_BLOCK_FIXTURE_SHA256` in the Makefile before -moving it into place. It is gitignored rather than committed, so the checksum -lives next to the URL in the Makefile rather than in the table above (the checksum -script only covers committed fixtures). See -`tooling/ethrex-block-converter/README.md` for how the fixture is produced and -repointed. +The real-block fixture and its source cache are separate, fetched artifacts. +Both must be regenerated and published for the new ethrex revision before the +real-block benchmark and acceptance test can run. diff --git a/executor/tests/ethrex_10_transfers.bin b/executor/tests/ethrex_10_transfers.bin index 201351189e5462b12704ee2e5084ad44ab349682..90eb03c15247a4d44d39ff9722aea9bdb4cdd429 100644 GIT binary patch delta 4997 zcmb_g2{=^i8$V};u{8D(%5p};$i7~c5G_bqMx&HDLu7s{6n9`9IJ9-t#=~`ObO2_kDlw`~JR}_nA2#{K839 z5vD;9)FJ^vo#ZYuc!XG*5Tp;*LJ6EBtEp88wgOJn&91J=AIgqJ_smKZF(+|6+yx{1 z#Y1Vaj-*-3Hl*9r>{P3cO{1!yQqnJV(C~aU732HTIa=q{WE5`yg^_2XPw1Kd~=5mzNa5HDQEJl$S3&T1B) z_8P5JaQY=1w6^$-v$Q0xHG z3&bDz-3783R9y~4PXvNUg0Mu;hE6PT-k}@NPNk($U7}FDhj?ZNthg5AVFV}SWd4su zcegv*4`^R{-4RQvafnUUA1$=$+NIb+di0Kn8L@KRv_4=!Z!dgVNygLDb(^kR3YO*3`TzJEwjxIJFVEFmm6J?98ahnCQPKt zO5(15@{AiP3w@kGYMUNeW7{&nV${gAxOlxK_tGQ)>2CnM?%j)8HC+X*UtwsQpnS76 zx)Ng8oj$#2W4;K*!);iJaFJ2h@G1W495dL;x)sLU{5^O9#|Jg6KS%k*&$H(*%mTr(QtCOv* zyW{Marsk*S<0W}k@5U&jF|KhEK;&-#dQ-1m zt|x8tJ7Ug9H43Brf<;IlY8zOtWxDX}>eB%S>L_m>J6_*+;zH#lfaqTWz{EiqE6qwg zyK1S=f{@=Zhpa5}@2o;?CgWJc*IC=)lLZ8m=XH7DX7ypl{&RZj>3aqx_8X*#K7GGv zALp7`vcoyvBmm`a0JiN4QaPnoT&14p%s7$Qy<6ed=mM?H*Bx7;)(36R)&7lOl$E7# z2>C6OXW&rMmUHx+4xPTx^jg-gUG~eG&rWMim;?~}O8|^`(5`5=u{JrU?Nq$Tx~+*D zoor{^=a9SE{wcc^$;GE15fQn~ejm#n71ze~a&>2VzoP||Y`rhGS7v>xvY5l2w5f&_ z4NEn_N!)qnrvr`XSFNvVrc44z{5OCL#)FCQB>^QN1#jl7QtvETBSD&-a#(35Y1Cw1 zW3R7un175fVW@m_`Y#_<%HGm{53M`saG`{yox1(yObu5B!^O|mFL*OVsukSM1m8?? zJf7e@A5P;PxD|U{OtIAES4PBMH;7M~($p<{K_ehOc}nwO4fAL(#21;;yxCIEu!H!b zQ<~8IUt2|*HmJxir8_+tD+4=lD8EQ7*HylWUtB2R1w+>dcveK#$+ zNn&38N(D?hKK{Gy%WVy}mhM{kg_W}p>d)swk?!AZUl|Bad=a@a{WDWCdCMMSsXx4h z4YUL3L*hDpb#rB#ekPguD0w0f!=mFA_To84{IBsKOusu5lCc?VHN|%HndDq7zr0hk zxw@EivngI8yrCXT3=FbVtBq%b0ZYNV%smcF_h{@Ver(RH-s9?VuQF3_oQV1}jj?tv zs)gUt$O_N0W`0Mb8S%_(^g9}5`1+A2zopUnEgYTZ@hy$cjV3ik!&5ZIDV93t`Aanx zd`qJ}pTDKi`G*fZ>UmIm4Ghnz~dV~B8@(vw=mo=?_K2&9)SjG%a;x?(*^qIvfMyq$FHKa z0SqJXWNm~h1e)+8a1#y06KweRpes!>Pq8q&bgEWT-tURg=1XM(3u8cb_Jc z6SxziRq_gb4j#Lro9{;E_N=1R3i35$!adr~%zt|Ivfz@o(3HUMt0G2|PGA+R&$09^ z%&4zirjY-3kw+X=`Sl)C?+SR)fpa`Xn1w#VFmi*WQdH!ND5-wX{na|gks6t_M%S(*w308uS>x8) zDtbln1!vXLs;)*zR?EMY_gmscw~JGi2#&VeFIQ`OT@V2}o1u^Bj5U^U_#~}f@VmOr zlSoacg=B+A8;7kwT!@<6x{TV@eb7RXPvGzMkvS722%>Y4hKpx7`Kc8t*)i?T8m%My zEr$0mXsJdG3iFhLO!UgV@Nh*hZ9xoyWVAm|aIrG{`ufM#2Iq?YVzfs`Ju!%0n6xT) zaqb1?xFuPTPT+;Q2w6zCECUf0A`$o{1|l|&N#G~-5D`qkC-4=s5iN|^&qY2xzV~j= z&-SHO?R!IwIMbPw`A)^_qo4ToIGNI40Poh>Q{Htth!#;W|AY#AHF>l#hEMgM&#s0N zVog5roZDJYC_fJ3R>%npj;s$u4D*G6ef{lh&W=z`Ox78BL;Htd`|oUt;o4Wk(V|l zX1U6XatHmHbjKzR!E8$B=~p~xFAa=DkG(!5;JnYS{@_B1xohLYoc6+GFZk>mjX{JO z3Gq^l5CdU&y8+oPrL9M!ul832xySNvKS+QM%9wp&^WK^%9}9B70Fp5gCQ9I_!kByB z_|l~yj3P+`MPHJN+F+3J(f=4H;>3@#Id`%w%ARK=6$J(@5H%!NvY~j&J1|r zfzeUVK`)P_4QYxC&dDGBkVxRFGlVV+W{d~On=w{g*O*drCNdivBFPcV+QN0}NA?a# zV6h!cX2Ta*zfP6_J&^p$&Zy&rwf_eK&omwbHU^Mp4;vmQuC))RdP=H(+WK3jg7WE( z`iDD19vyr$vXsefCh*V3hy&3#XK0SqOMG`Op7)m0J+hvq@*toDB9I*Ijyd$Wl>&z$dO&P%M}_*)80sPS^*O#0InNr>T_X^&QS2Y?e&2c zv0PihUF7!c$oOmBr5>sa^S%;zo{2EGdZBUKfXKE>Sg5hS?eN^)5l1i8mVaj4-Z(tX z|E@x!q>Zu)VA2$r$r#^By{MUiU5yJBnKD5XU9gDJR-G$W2=&O+gM5(;zKKJ0io zMKqYjfw>8!!~_yoBpu+UbJ681eYiMRB*V?k%@QrAp?g4qz|UNGnsVL50y=<8T3r<%$UHWiEaKQ6-K%>x+tN3$>eZFzfEjM5O#4WqIPs#$X zGr|QmPmkF)emQi}js25%?~Pi5`1EmEf4tmj-MFdc*^=ihkw0NQe>b#bZsfE%BL}^o zCs)`RARy|v@bxJOWz8B0#afAMw%)!p({gfLoSKh*QSNbOwnZhHToGOPJ}L0EBI#fu+ph%#v^@}--fJNIu9b-7uYZma zV!LM!%gyz)2PYD5mvu_^a2$O;th2?qgqt^m`)mm*UQtkU^4gQc$I~}Yc^DrQb7k!9 zrg?tKobfhY#r`cI=(F)D7NpwHowO39pWXD}kJB07&NZ2{cwxk#!a&c};((E|2LW5= zK1?|i9afT5V&8npA&&|kZyj#ECfx7(xw9tc3Omm5O?KE(G0LU|1gt$Q`2D3}VTe|O zy7)CO>f-L6``R)6!W}S77V>ZjlR&*#c3^M+ni@q~{fUAfR$m-i@l1GbLu779-Vm1_ zN2Yj$HAD?8IASlfe$~4L1iU>E;@@c?%+N|?&t7Cvcjbj0_CdDUW2*(}>0XO3N4byp z9hbK({m^1$V^O|WO8?>|tB3u5A5ZQ3J=wHU-qj)hd;86&${LRc7X`I|AZ!nW%(of{ z=~{_gw#`~Jby`CC(2Mas+?%hMepkEf2YbI=T~Fp%@9R8u`NHJebl$ZQUoKysUhFbN zCbP@W+*Wzet8dZz%TY7rg@yJlAQ0_=Q1V6tp-3yi-P|03`FE)ip|ye~w=6w|&s;Wt z61H?-<+hZwGd<0#e(B})+ngBLhT#tJ#I5-UBhBi*?$T>*ebT?ohaPzqel07m1q4xh zAiVrj1L3Jwg8n;`>Es>>;R!)-0?c>t6{n6VsEhM#@;xmQJX-e*Z z!E09_rR-n(W2`DiH`SPOS8m$*&v$GAfou;1D{W{qtwbiyi-`GbVtvWre$8kX$>!9a zC^^CBI%D<4vprDNXXdBA?MPxIN0C%q=*4H!^Wm|jpE%)hmf%4KF@cT(>w9O^5h~3HG z1^W*9*+0sn3a&d<6xlm$$gyfbQ$(SGsn7e$?^w;G@rZndIU;wa`CEt-%X_6%$`ySm zq$t*IfSMUTKq{s5#S@V$9C%4LDFPcVlBuQyyy1^?Xmb_!9aK1tk$gk3whdy6>xJ&u&j5B*Hw*35ZWtuY|CzTzFlb%O>GGW9BSXhM=N8uubMp1whB!$u> zDMCn;Bw1E~G6aK)C=9wGB95T}&2prG5(7X`tcauphzY-)?#aipQVaAOc=~uic=_5> zXdjf`a+x)%nzFR5H{f4))OP^Bv#F6XSfv1dg0U@AV>@js#X(0ndG+9cN6fU}vG;X# z_cmPLcobp0^vlMkU$ZN5)n9@6(*zv zG>&5?JR=UaK%=!2Gd^a6VQhCH&QTPJGD4cc7(_tf9E0IFN{9gi>7qoW0L4Wj0u>QL zl4A&pW5k@0g@I8HLsiUqTc8Wz;O_qwYae5?wH@&8l!h7^rsvDx_90zP*fF@Ww50m# zNYy0{@X;z0;8Qdr*9>N%%8-Q@8ncPHHW`}ldLSUG*TApR)>2_X9b$U-Br7%C8oP@JZP z2+DB?tU5S{3kjU&aBY>zU|^N`zwQ?qmeHD3xMYtFzv_G7t7>kICzW`QO-YaN1Npx? zOucvkwdsG`lfK-lqP5BOYq1fVosEefY*0Q%t;%kP09W}mu6lCg<~b>0{--T|aXUI> z`otPZ>V;jdCxYy+XUzfwT2DZgkUb`?bD*+E8Zc=h1AfJaCSyo|&-qlh!PN-?9v7e4 z?seIk9UJu1^XL5J^fA!ga)^Zzo?BM=jJ>LR;fw zajLPhQl$Vs+hEg2kYQI;r=HCELrhLs>LYz!KcOkImFBQ|kAJE++gL!ot^w+buGl|b ztL@$haI3E97<*tByP(J}ekqq3^Gz9ec(yUVzc*dgZLREKGy-ZDmp1qZHCvhnUJZU} z7B|Y)GyuEWbN15W+CyWkhU`*8u*DW?%)hBZ)WUn9_JpyMKwW<<0e-qf?Y2rHP13ED z;+sl10052mluFg#&#Y>VuCl2f-Cwd!*I3nWz3G2wSQw214ByLTmR1H$=VP$Q6%nC& zP2XKt@1kz2{##T(k&)9!sCCu#S!MfB4+u$8_rpsTP+bu{{C`yI=J3tQ75)-gMuF$R a8OM{ioWh;m67jbXVH3O|!t0Jm7yUmySq(h^ diff --git a/executor/tests/ethrex_bench_4.bin b/executor/tests/ethrex_bench_4.bin index 23c2453ee946e46c1b8ab3d8b9fca79d03b11d11..3a0cba1097ce1f6383a186e7fcf4361804f14322 100644 GIT binary patch delta 5056 zcmai22{=^k+dpT6_rSt#nz$>XOh=?3hAXyQj}#- zRO)?+QYjS@QoWW}Bg*%jp|9TW`v2eWT-SY`XYS{B-@ki#&T~E2@Z#>RS674;APBlB z0zvPI14J-|R_H>I5f~FhP&15Ig1CH|*TdjroxwIi>syzMj#lB3(lsv>_Qc7OLNT<<1aHQk!#vpYvR zfu?!_Q+Rp)YfVytPN~-z(`;JY1BFG5(0L*n|0;9^Y1CMFp_n=Wf_UM2X%j;#DudZi{!Gtls5Q)% zf3yA-F+sX~d(uQvDoJkY5+)fVy4d7M+4+p%Zdi<@;(9tR|62z~&VwpCz}@e7A@d)U zgny+Z2pFr}pA_v;;WJz^S03GXjU0Vqxsr}HYcNVXuT;&sf+rr0k>5$>)%2b2+0ocA z8g4%3X=(dp(_@X8wXTN?%7!BRWTNEOi4i z@u1IZWhw%hZCTGOW6rxn;RN0tCQQHq_;T>mXwQuusVObu%bNV=GiBK9e=OgA#c2Lx zZPJP|Bl()!^Hl-!vT+fkPFG$GbTFCnQ}9)3Ew3Un3kz_<B+LKoS_vPh&{qIUW4XVLLZQDOcb~p&w35pvDFvn5O_f{{JgdKsNxCG8zyR z4dyLCG{Jci0ptz?yp+LV!~*gGh%Y#Rct8w=ASeTfxG)4A2J#umR&eTe0`U}upa(!! zib2p9AP>YLC|3f4IFb-l2-qs6A?TqDU;!r>MGk`Q$wLs|i}~}x7PQBynGZC{L&l8w z!~ws$gxj|jJSuAEuc$NHxz~MnG>wVrTuyoZKI*Mka9qcE!?$w!5wE6RB7J+U(x>-d z+|%7+4!r&16HC<^H&_vFQuwiYsdtF6#`?A6lr+8(-Mt_LiMuAOFwSIRw_N^Qd)e=` zOsbQ_!I#o08CzVTvR*V7k(9zCn{$St#%hQuKyUd>_cP=EPGvf!Ni9x}Obo;9eZAyP zX-geZd%rK@f5S3*9~=KV^IzMXOg6Uurdum4%_ps0!z}Hs@>-o4X^TMNU01RGxtv> zMddYL{B)ico(4l8pW=^}JO(}dT%t@^nQvZ?ph4dg0ZnF`DJX`!QPP|7}kRU@^gK0=~a zepo(ejjxsyLtQ-Vp!I3FM#ua72)x-!W{94k?3nWx8O`&rHSJ$UYP+l;8o$^*Z8KUC zW!vd4H87ZI$uGiCA#=oVu0{NaeCr$K{y7dZLSK2=P2_e0m4DyE(gmI%Nng>kmS|P1 z^g!*5Jr)C?jh0dVl!AS$H@C)Y21=g1a{0WQdE7gFt7pTk^JN;dKplQ0C`T8oT&|~Y zb(iefPY?4RQrcv@%CZUA#A~|JzK>5te?dOF(Y*BJ#x3onW%iGE?a^jd`%R~)`x7Uz zJqI;=8sHz~2lJ8Uo0nNdr>aioM0dL`?EH4xa{9Ex<663LNuhG6nL(v5ny2Ke!;i+0 z^zNQ`H*1rbp>3Vbu2o|f>7Vs96GF90k~b8rE~udVf^4{?B_EsrN-cW-it$de6lA6} zt>WDFjdNDnEw9VEVHPRGF9YR0y7)(VEl-TvW?kGWA86E?`rWs7XP0N-8E>IYd)CI$ zTMr)Up<-y14gzCnrVavjKWNQY=hUq5Nt5n-OB0gvp5$?dD;#FhXyWs7j$hAZ=pK0s zK9M#Ib<;u!fG1T8(Pv^MgpaKfPfH#M&p2lnQ*73K&E0g=H~i+oc!k=*Gk+0+W}Anh z_1ef{?7UsEDs3b2`@QHksV#Q2S|~o&>0uetM9{9oDK_`2^jcxV06WjDWcI*2OtqHclP+b#5SP(thYJ%uto_gvTp+hZG zM0@=~v);5Ir9}l#uT_q!JYS=$MJh{Z6a<8!wM!5YCVvEHe7_Vgg18O-HWa+q*&wc9 zrL0Zq>*MTts?lAC6lZ^~-|DUJQzw@6p3Rv_rW~kQUWJJ!7k`p6I(bDiuficq%i$c` zPI`_1mcFU0QHRLq++=2x$I!F3Ai_yl4nw;aAS;>J-VQbM)UE+n-KWMo9|S&*Iooah zM__SRef&UJ;P+FPw`18 zJL)D9-C-X=p-U6Xo# z=)l3%xO=DZc?Zfn^A6UJRfT%Rw*C1m9?F!pn4-aOLvd3$qDP|cT!$7=9YJGMjpRZs_?^?ANQ72)oip{G6^sq?6+ffQ(ro&f?5 zbWCD+^TbrlBc7k_<=sapvQ0PRGKD(qzPda%FV6JXRR0G`>S&2F0e3WbdfN5q6=Lc4VJ>1j#GR0sbnS2F9ugc;LnuNW; z7oZC)3|u^TMBli}_k!PdHCgtrO#fo77GKO%{>@^CKYyn$op#dzLvu;EbsY)O1>6xB zSuw(qST4&;r?hLJ^i_+}_eO1wwJELQY;T|U^zP?u3^h=gCE_T6AT6SQIJ`mKq;bx$3~FDD2*jsHFJ-Ti!)>UDU{}^c|q@~eEOM` zcTP+Ci*2@yVkoiwm$Jbv(354mU8Xd!zs76C7Hp`D$f;6V zx1#-m=f#%@-PB?qswFxf ztrqb^14V*Rc~L|7DC!`(WPzyy{V3Hc2tGmj18BZ9I_wKveGpq%E-xBKL`TJxi9xUkDk-kd zAdsnam`$L=99jU4EvHBiqq1O5JRu>Tz!6FVylEWzdcOb;$`MNU^z^i%S1Qntfn0zc z8Gp^?!UW0Su9jMc&dfbZ%&p)*0!R+m3uXiknK+9A!GeT9P(%iWNf*I$rCG9kaV0W0 zI?avrxsf?HVwN0VdL@l!4PF>-K=g$I=tPh`g~=u|gJ3`iYfwGGB!CvcVu5ls3MwjK zVbLoIED8(M9~2jcDJ+fIDFFu7-gGDRpV71E^I+6kY`G;4B?9OyQfO|*p$o$lL1Tj8 zh5y@`Cv%;713E)qN|6518br_pdAv{ zNO|Wz5wQ)SE28==~q_uYK!^H6fclqI!oO&gzm>J?HOlVW3D-pDIn)s>~Z?S(F6K z^jAgr>D<%5qt#7!)E?& z_O)Hz+xGS?^hx=C&a9!U%F+2=UlC)l_>PHCU)Oa@>4c%$`loSD1)0UiPwkG#O-ge{ zr!Oo!+BK+mrBb$k=fsA@%wt6>x7cwvOFolMqy^`tsK)k|EdNgA@P$%HzsK_RPw*bw zr|jo?7np>(ZR?fbMi!iQEth>~?q1?lAhfl>)C;vv{W!MpFz>^m_X&TkQm1r3)01vvx+#wLIv z1a&zgAZJ*~a%_U2h%BcOMJ~CQV^P4EaLvA+o|#NCL2V}i$F?JM1z3_?C=`^Ra-4uOxFR=Ffl_fKV zZ=vrpr@EdET3UD4?wk(}6;Dmywsj~Iy(BOBjfGc)Ua5f#zJ0KKa#p9X+9E!kzG&5^ z%m%T-1jSJl?{5Togb^GeE-MZ^w5-_Urf9rWxV3e}+T7@jf4>u5E2Pz&@rj%J&T6xy zV`^CD|stg%FBD5 zTfH`I_lf9H7tqX7zHVs!+B@IxJY`swR-i{(fm~SF+}gw36emme;x!*V^(%jA&egJ+ zv-4x7Cnrt~JGGga-Ta${$IDhG;O{RTdt>;(VT)px7qw{h@J#26XKfD}q{SVhjmI>4 zW(rtg8zSxKGwqeA;e7{RjyTt)Vb8zuZ}#s}|7i30-btsDf_F?YF2>j#_TYipKlWUd z8m;~uJkW9E*TA-GN~;y{&2EfOKXd5r_LXm~n|UX8Paq8qsm88KiCvJ7GY=eY(yh*` z8z#k^E9|v8qX{#yQU7mfo z*IRF7WM$!^D?>Jyjk&-XsWf`ai?Mrg`&>}@GC_!!HJKB!LI~mnNhXRUiC7|%L@7g5 zK_#Nh!ZIvss)7`%NsO!`B*;{hWCjNq%jI?ot2*gZRfB*LCiq4RrN&1 zyz1TwWNh>>?JycXqhHoX1LhVEi2R~u{5t8~x3;bPw3|?xoB43uPhI+Mxs6enKDeut z(RA2LX>_&YXhM4XU&bEFi>wn@kI%WbtnE`{-CB{+Gz4@YBQ3Q zQw3R5b&aQ>m}o2sFa}6*Mh(dpLMDNNq(6oCqx|46N5->)1&^KeVphJaGGqSG6seFJF}%KD1f1eA;2|75~-vLBGp(K>r~LG#4((% z!c(I}(Rf6tEXynwc!9x0cDr&UAl(v>uIS35n(P6X_=5;vC3@}jJ;39RDYA`bqq zLB?6P=PbQ;c83Q}|HV~Hs?dpyu0h^&AY*h8xM`?$cyM{2(XlDn!B6(vbo_MLYZE_L zC%0E;&1p49y4t#M&b*NseY~moZTU;8R&9FmuDJ*qHLCe2Y+23Uvq_W_6b*5d@)9Fp zm8FzuqJR}u<26ZVF<}YAi-c1Y9urN&P{cTm;{{I9I4D|XBHjGICz74bt_p8tI~#2k0*?$C4=OBhZMAvscJTCumjlD!**$&gvdLTKba!5Cn-X-bMeR!mf4Fa} ze{y@B55M=8i*(N>KOZHlRN+;D5k<<$Jb^AnWE2Jp0I%{Ife=(w4vs9USmi{-=#q?h zO@J#~BPv0V{H~b%D;REeER|z2nU|>o?GWIm!KKUcR6)87n^hHVWR+tnMmkj#EGdY{ zNav}}BMeuDMk$AcKX|(d=*$2MLOc7FCqeWO*5mbEG}MO_^fUg=pQIa zJfMM9feH%C!f6ymA`lN6C-Q;>wNb^gN>v2CprXhMl?kR;pp@!}X9dQrn9$}uUfY3; zo{HBRfBQhE-mU+Z*S7v*1Z>|wHthSeo4dgALlq{_IPt= ztAfXmb-(io4 zEayCH$|a7Xmj`$`@AQqI8@t9lJk1|y(qHN9;w5;Rvda7meW$qQA<~fy|H&zvU3_(T lYL23mP!qi=~Q0VP+EU@LRLq`;(nG( zO84|b?AOnH3*Kx$@o7(+7l`<;I4Xbc#G;^W?q&b3T^f<-W*-(jA<|5`;-NrnSn(~KWh;O?@ocq4Z4QOf0p{j9xt$OsSUL% zA6*?GXL6q8dc~-m(++D--5l&1cMqywL&C-jNPkmoK8on($SD((HSNHkI$f!iug!QBy}W>jsgCP{P`CSA z-}sN6OIy|%${Zd!=rdVPkwvzcu?`C)Xs5_%YYYJlMKeo}HD)k#NRij)VDqVp502SN zH=uu9%V=lDeITue_GEg#Zv3db^BCtrlDdXPXBXLEF2oefqSE3a|C?XD8}PEpaCr4? z*SeaMF}KIMU)C%zsU!0r1Qr8kiVU&SCCs74^U=N}cQDif1d=<`ttYlQmb&DPZtw;NX(##Qbe zpWnaU=UN=q>$Ioo*2YJj{xNZzDU!{^9H}wu!P?^Q|B+fdKc_IDmGE2!m&Tq;zW(XJ z!@NbWRQ%Hv8NW{3aF{O4pWR0ph*W&K~D6<1SAn%TM@f!PK5JAkvBVR>DsJ7%=uQ+3Qcr2SDrz~i0 zJJt-hf~C86GmCZ(v9JZi6vCoJSB}T?O1_*hA~FX>BJH$=Y)Cwbf1CGMuw?LF z3hY5|zV8T6YaRVT72CLE^r-15s=m9WY^_lZd~IDZ`VwEek7E&T$_&>2Ty7 zMHcG7=V`FH$DOY)I2I61nNx-rQeHcSMpi zw*U{Mhn|~(hZJY~;j735J*Pk$u~Gr!D1z(33Vao!5aGC;A_8ZFCm0ri9R{m$1Qw|y zz%$+-Ez!V40b2&Uh=Dsw2J=Z@eV-*Vu}~_(VG)ifl`u;Nr-_9^sSMs7ErD@FjY_0Z zQXGx~{Gbl0^ey=@%Bi}DW}#qX!;rxYG0r0U8BTH;!WZMP62W1$R3cTftnp;A0#@%q zviBfrJq;*9qsGIcC2CTwS0EG$eer5Go(t{@;`sD5dshNnmZn zaCH%(LWZ$%G%x6CHR!Rft?wJGpc_mmBX9xteYpx0w}8oB zy+9ufM4_`WVdQQDKdep=61TiT&gVt`%1Y#?$NxbJpO2wGJkDiSRA`*jr^pvJj#V!W MnqipIU>HpN2d)q3Pyhe` delta 2078 zcmb_deN0wW7(eIS?~ik?BEB}#yf+ODT%rPPI)Pb1iq!~^#VmrEEpbWCY10-;;}07n zUeAU}#-fbbDkgz6iHJ8{u1qP{=9Y@g{vaLAkCBBGw{zb6zW03-+N^cAd!CPT?(;mq z-+9itdlnC^wD+E>Pu#L>_sur5^x-pwA8HFHj=lN$L+cJ_WDIsU9XVb?Qv-I;Nwklq z1`j36UB$n)TeZytd;5krboMN2c;vBkaAMxNdtevc9kBV^(VYuLbHa6lp@ll^uTIGT z{NYnN46AH_!B8sAzsrV9Su!i_PuJx$^CA)^g%x@oC(&m}1vN+!{eq-*Zktvl%yaRMC_*PV%amZ~foxeKx~?HpHcZik zf-VtLlq95Bz+`}QRTGe;stQsSStF*R6VoCx)+_-L5qa1eMTTuB!;<~4u>BL}$$miF z1rLBD`lw>dCV}FROF3jfUydS=vJ2>2-b}A2F6fM^!5CF@jqrE5T*Ok$hhXD^%(V@T z`m@E;H>V{l%|CqSrd7PYylYdHy|1s(-F7mv8qhi~LYrYvDl8jIia~&cv8)LSG)0UN zQ59&IL?)UgLL?$VR*_^FGDL(x)>#sfWJNNF6zQb((*Mo2BQ{6z{|#}^s)0w_*Ip!V zj+|~V2mvw zi;Cd2^{qQ^Ye2t|eUwvTam3ze^?**lGvHIwX{l2rV(pokkb2?Mth({SoZkutgH6R{ zAN4ONJKUSKX8)_t4Okmr@!PW`768zF@y1bZ0QzIh=RBC+nOf6a_| zosUhnS@om7`w24~x0<`@SlFEh!HSFL5~|w|;+_2|TWd+n<{jUHOUFjHF3mFr?`?E0 zRY7oh*U9Sz#S=+0iUvwe=kCFFP4xQf9dg;{0E`T~uE^wuE1u1L{23O07PySsT#r1y>pv)gXb>)A-v^zf z*)c1+Cgur-D$9M#pZ?|AlCUnbkr%uEg*9PYVLT5C{ka`{}wrDijSM7nHfL#dpRLMbzIrnX1q z*j!plZMKcVP6?I1LT$>%rXoqn<*Tt$`<|h1cYD6)`QGRGzwbNm`}_ZY|J!-qsa`F1 zXX(Rw5Cr+sA?O)xkOsEUEfxgXfz^~wbRcFHrr;{JqIrJfm9ojgbS&3Ai_SrWA;I$L z!zvr~(w8FU{+q;Jdfep?B6qwpX6yK>#W;KO8jl$Tu7MlHDx0m}viHPhf z4kNVL>)3l~L96fP=e4OWX$jZGH8L~*aeb+$_121g-cu392Qr}XG7*#<{Ez!zCK9rq zB!`Rt;LXX~mb*y(Z|ZU`V4qa68f|vhYTJQ0@eM*z$i{h~AAj-~C;wlEQ0_mMLYf|L zx)$TRM;(hVWYk{TYu4U)3c1;qshZf@f@j6WyO=a(+H3*P5oxXuFE|zdrX#)B{Pp+j ze(9kb6)B%&ES>Aes)U-Zz_3gY6zo%Bg0n7l2%6T1pdhe60ptx3U(f)BFysd@Kr0Z~?hAR!Pv2KX?5OesTZ_Y7it3#{U{5QKE(NaMH3m z@4>T_zTnv1cgq*_Y1<_AOg%)OZgG*k`}Wi~y={wu#^3bFeA5YA@5F_rFY7-KiL*Be zTrt5D%k41#6LIM7b4q#>I8G7_Hp--kX7xy-$^Z=o7cnDPxTExk2%IZsT4X2hO&~qO zg0&BsF?Y>W`P4&=vP)InrX9O_UdL`#0Ou2Z<|qaBh-g&mNpNKabPguf z4?LgH@pj4N|E%-Murw-sTVwDn*ocK%$`eQwt`jp|NBydnO1TpS$qjDi5v_iYTCzP@ z`YG0HuD+N-FMH&6E1;vHwkbXKSftZ#ufQjC_?PiV^_&Bdp(?q9Oasc&hM`DfGo|g{ z*Bv#oDY|2~)4CLe54EZF5@(yasI$C`B-UG^3udbzPc24UFH{T5 z4%HnjOzT}auWS0S^Si@eb~a-6m8Aypj<&U71WPZ>Ql3VllHSLA{9G4)7+TlWy0UKk z6gFbT&Wbm!%wBb3`HAzq86-_ECJ75ulty{A-xO7%m`H+S_K}nHy#B)S^rMBatr zVaJIr>rNLXb|@JX^qIG}tv|3oAD4HK#C0}`05$b_iY0g+syyQ{NqC~D1zG+@g^^%2 z?ftd14ra_3f<|ahy5n2f%AT$Y!{6dXb=nkVD+%r%#J#pL*KI0j5zZGRv z*&4N{Y;Y&Ew$*eUPrW=|7ayG7ar^!r=%}XClt4D@WLOd(d>#biNO?mN-D(OlHWM|+ z$@TmAhe<)6Ou%aLusWrhwX?SDhY)4?@m=pz`vO+ijFUtb6F7BIJ!O-<7YI&6dR=x#mu-`Jw4KXE*P^dRI^%xybNetx209<^nAWCX_Ce6Gqpnx zHjL*W-PW4?7aGDYP+A4)JH^blr5xbe0tb2~=sPds8yDGmzH`q>I)1UK_79ue zo8PI$+||vR<6&q70;6D>i|!n{U?#yYr%$R}NP24-9FDMV1em>b!?HDdeWy%7krg(F zY*8cJ5WGFxD5& z7sI$jFB-Oq5MfAIG|VBG3}@@;h#&zUgLxQ|nF(u0!zmF#L4s&_XE-0m5D|?p2oqp% z7%-K`FR|!mwI_z!gj+r9caV_!tdD&%-lmcr*+M zVUvg;u;B~%LLo?3lOm%E79m!P2zf$~e~?@l<_S$^wg?QgLohDuFYkqz7EGL1@^Yr2 zGyt7J(r0%fObO;G3KI>_`)_4_ovq9xP#KOZqW-ZosG>B8nW8j^HB}n)uhP^apc@2& zY3kqZBqAgnRU**_u3yZOUotrC(5KpQoYb}NiJ;eGv>*C&ndRkT>(oiYLC>tFO{E!z zN%0%h8>j#A^w#swxc$F+>=b{y>a|t#q{Xk%?@qNhd@n0(YkVE$aInz9*Wamn>&X7d a)a%R3d^JCaLJb;!{9wdh8ye=~67nZY$P@ek delta 3010 zcmb_e3s6->7~b9U*mL%rvj>fb66U?8G9eRa_(%rGG&M~V6-;u(S3Y_$Acc-*NH&&} zmX}pCQZo}&iY6o9ie9zRp*IRn>6lHWU{aW7K8o9(`-Y2YO?T$*zyH7gp8wzP+ueU1 z{XfXM8=VvC1rYqJE=hK)^#wztct;uSut!O!E2@5l4rk}|4KA0<8ZPll(D*H?iCA2p zQr;ZDbpF7D^8B;scXzuyV9$kv`N<+UB)->*Jv9l%<7d>AM_n-R)(I=vx7kOZ zn>I6lMBl&WI-Ie#;?I|c&+cDzrA!|;s3!eAHNN@K>U{$ze3+DUdi$zvhcj4Sw9R1; zpM>|HoW0>#ZO-Nabv-K7Pk;F4>h_-L9gZY*8G8I+@qS#_&6%*PTLZdtsxWa=eEp(P zP9Qv7jTc-Z&ewn3@b=-ce=NK4Y}&r!itiuDi+OeL*d7xT3(1bvBVD~uq_2AWrx&ho zbw)r(qQ>f=P3-fR%jIy!6L=suHpvYL>#5iXba#2f(THQO#n>S4^y--Lr^{sx-OXoN z9x&S5VPPGj*v9r#*y#3Sn0!Z%%6;Kugs$6fxLlLDo5`n*;WCTMt9<^jW|wOnm)B7C zdWX}-a-;j&He7p$y}NKo+KrsG<4;vIWnWnS@trlLB?mX3T=VsBOI}N4iAtBshUib= zkaaxf6a;`wD3KJXAp|6>5~QiB0#&4{h9Mvw>qsFYcY_MmHJ}+(6^ID{CWfLC0fsRr z9vq40ae~u$YFoWTZ{c#V!`=y53%0e@0+wU=2LM~{QwBpYbMfOV*Jm$1kR5Xc7JKW;}%2 z0ox{e;IWH-%mQw$AxjW<^Zn=rcE1l;54@brM-$eU9qk;JURphJi&(c}l>NlnrbidH zdqy>CD|hGqz7T@@3O*{oOP_M^+N{F&`y43@?>@Zx;^O42M><{}KC7VU#a*1gt|sU2 zbb!Cg! zL7b*>2B-UQ0jx~(uOG0Z0j2HG7PG%tPJo$u(8Qj4D6^@Cgf$8z!;n=0LtQisL=_A* zoyt@-MTkTs$O@7)O@@e4$nQ6aN-~x-D)IZwI%Dnd|9!^nVo6(g19ndfTD)ruf!bPm z4hdTMa-YIUC`XN7k-G9~$u;n;O=IsLaeeKCS2A*U%vls()$m3Wn%v@u+#KL<2&X8i z?JPTKp+H+XomBlfc|HtSb8Ax|BF{B>W!Q)TnVC=IpFE_gwdJQ$zV25s>CJh`v1L0y z+&i%IgSVOMFcO+is;u)mregzZqDXnQ(p1DxGL;k=tDIsT65Y@NuXdV7G=m^$Kx`O> z31krq7V*50-QL9t*c_$R!UL;xAH|^a1z6QBod1dS%)ovR4guJ_zyM%tf|b)jO0iW} z0kiwcYtTJm7r(G-p1o7+-IgFO`_VnX`8pO8l~=j~i@nEA*A>!?m8*)8$hxm&mW*obNok^`f$VzBj5*_gLMn-}E0kkwM=9f%y)5 zbWA|=`2;Ku=ag)}<{xfs>gDdsytN+G(`G&5)>YnGw$^%jR-t>Gzq|n9|Fq#Bx%CL% lQipSRl)Yrz1HGT#vuI6~l$bI{y3RAa!k>-&MWgtl{|y;RV_*OP diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 0baa21db7..8c1fe7d2d 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -148,12 +148,11 @@ if [ ! -f "$ELF_REL" ]; then make "$ELF_REL" fi 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. Unconditional on purpose: the - # target hashes whatever is on disk on every invocation, which is what catches a - # copy left behind by an earlier run in the same rental. A match costs ~35 ms. - echo "==> Verifying ethrex real-block fixture (fetches on a digest miss)" + # Gitignored, never in a fresh checkout — and a rented GPU box is always a fresh + # checkout. Built from the block's replay cache (fetched by URL + sha256): ethrex 25's + # guest decodes only the Amsterdam schema, so no hosted artifact for this block can be + # valid. The generator validates the block through the guest before writing. + echo "==> Building ethrex real-block fixture (from its replay cache)" make ethrex-real-block-fixture elif [ ! -f "$INPUT_REL" ]; then echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" @@ -163,6 +162,30 @@ fi ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")" INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" +# A workload the guest REJECTS still produces a proof -- of a program that decoded two +# bytes and gave up. `run_stateless_guest` cannot fail: on a schema it does not +# recognise it commits `successful_validation = 0` and exits cleanly, which is what the +# pre-Amsterdam rkyv fixture now does in 496 cycles. Proving that reads as a ~99% +# improvement, in green, on both sides of the A/B. One cheap execution up front turns +# that class of mistake -- stale fixture, wrong fork, fixture built against another +# ethrex rev -- into a hard stop. The floor is far below any real block (the current +# one is ~20M cycles) and far above a rejected one. +MIN_PLAUSIBLE_CYCLES=1000000 +if [ ! -x ./target/release/cli ]; then + cargo build --release -p cli >/dev/null +fi +workload_cycles="$(./target/release/cli execute "$ELF" --private-input "$INPUT" --cycles \ + | awk '/^Cycles:/ {print $2}')" +if [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then + echo "ERROR: the workload executed only ${workload_cycles:-0} cycles, below the" >&2 + echo " ${MIN_PLAUSIBLE_CYCLES} floor. The guest almost certainly rejected the input:" >&2 + echo " ELF $ELF" >&2 + echo " input $INPUT" >&2 + echo " Rebuild the fixture at this ethrex rev (make regen-real-block-fixture)." >&2 + exit 1 +fi +echo "==> Workload executes: $workload_cycles cycles" + # --- 2. Build (or reuse) both prover binaries --- need_build=0 if [ "${REBUILD:-0}" = "1" ] || [ ! -x "$WORK/cli_A" ] || [ ! -x "$WORK/cli_B" ]; then diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index 5c216acbb..ffa6e7224 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -854,9 +854,11 @@ dependencies = [ "ethrex-config", "ethrex-crypto", "ethrex-guest-program", + "ethrex-rlp", "k256", "lambda-vm-ethrex-crypto", - "rkyv", + "libssz", + "libssz-types", "serde", "serde_json", "sha2", @@ -864,8 +866,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crossbeam", @@ -889,8 +891,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crc32fast", @@ -912,6 +914,7 @@ dependencies = [ "once_cell", "rkyv", "rustc-hash", + "secp256k1", "serde", "serde_json", "sha2", @@ -921,8 +924,8 @@ dependencies = [ [[package]] name = "ethrex-config" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ethrex-common", "ethrex-p2p", @@ -933,8 +936,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ark-bn254", "ark-ec", @@ -947,6 +950,7 @@ dependencies = [ "num-bigint", "p256", "ripemd", + "secp256k1", "sha2", "thiserror 2.0.19", "tiny-keccak", @@ -954,8 +958,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -965,6 +969,10 @@ dependencies = [ "ethrex-rlp", "ethrex-vm", "hex", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "rkyv", "serde", "serde_with", @@ -973,8 +981,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -991,8 +999,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -1009,8 +1017,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ethrex-common", "serde", @@ -1021,8 +1029,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "aes", "aes-gcm", @@ -1063,8 +1071,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -1073,8 +1081,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -1095,8 +1103,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -1115,8 +1123,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -1691,18 +1699,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssz" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +checksum = "b4bdd6d63ed811ae164966de20810be780e07de784a4834ccfe6be90480c369e" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +checksum = "aeee1b9ac9200429f7e9830492765445989ea61c3fb9028ad5a96e1dd5f5e913" dependencies = [ "proc-macro2", "quote", @@ -1711,9 +1719,9 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +checksum = "863eca32d1a43e5ec41106a515552efa8307768d37c68d26b7f21ff13cfee1a7" dependencies = [ "libssz", "sha2", @@ -1721,9 +1729,9 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +checksum = "d4231ac301726840a3fe111f11bd4619d3c97ed155cb94c88dbf92b70e04e017" dependencies = [ "libssz", "libssz-merkle", diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 28f8d7e1d..0dafd4ec4 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -7,15 +7,14 @@ edition = "2024" [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 = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-common", default-features = false } -ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-config", default-features = false } -# Only for `NativeCrypto`, the host `Crypto` impl that `into_execution_witness` -# now takes. Nothing re-exports it, and this crate's own guest-crypto impl is a -# dev-dependency, so the bin build needs its own line. Already in the graph via -# `ethrex-config` → `ethrex-p2p`, so this adds no compilation. -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-crypto", default-features = false } +# Pinned to the SAME ethrex rev as the guest so the generated SSZ input matches +# what the stateless guest deserializes. +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-common", default-features = false, features = ["secp256k1"] } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-config", default-features = false } +# Provides `NativeCrypto`, the host `Crypto` implementation used to recover +# transaction keys, hash the BAL, and validate the generated stateless input. +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-crypto", default-features = false, features = ["secp256k1"] } +ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-rlp" } # 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 @@ -26,14 +25,11 @@ ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797d # `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 = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program", default-features = false } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program", default-features = false } -# Exact pin: the fixture writer and the guest/executor readers must agree on the -# 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"] } +# Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. +libssz = "=0.3.0" +libssz-types = "=0.3.0" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -51,10 +47,8 @@ serde_json = "1" 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.) +# The LambdaVM `Crypto` implementation used by host-side parity tests. Off +# riscv64 its keccak and ECSM paths both fall back to software. 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. diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 48c58e27b..2b29b837f 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -1,565 +1,62 @@ # 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. +Converts an `ethrex-replay` cache into the schema-prefixed SSZ stateless input +consumed by `ethrex_guest_program::l1::run_stateless_guest`. -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. - -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_mainnet_25368371.bin` (real, default) | -|---|---|---| -| 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 | 8,734,622 | **50,781,557** | -| keccak / ecsm calls | 411 / 80 | **10,478** / 116 | -| keccaks per ECSM call | 5.1 | **90** | - -The last row counts keccaks per **ECSM call**, which is the ratio of the two rows -above it. One ecrecover issues four ECSM ecalls — `lincomb2_with_oracle` in -`crypto/ethrex-crypto/src/lib.rs` makes four oracle queries, each an `ecsm_mul` — so -per *ecrecover* the figures are 20.6 and 361, and the 80 and 116 above are 4x the 20 -synthetic and 29 real signature checks. - -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 for this block on this ELF, and spans -29.7–38.2 across the three candidate blocks when all three are measured on one -common pre-LTO ELF — a ~29% spread.) - -**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 -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 -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 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. - -## Getting the fixture - -The fixture is **fetched, not built**: - -```bash -make ethrex-real-block-fixture -``` - -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 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. - -Artifacts live in the **[`bench-fixtures-v1`][release]** release on -`yetanotherco/lambda_vm`, fetched unauthenticated: - -| asset | sha256 | read by | -|---|---|---| -| `ethrex_mainnet_25368371_4f658c2b.bin` | `0a301731…` | 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 | - -> The asset name carries the ethrex rev because the bytes are a function of it: the -> archived `ProgramInput` rkyv layout moves with the pin, so one block has one -> fixture per rev. The pre-bump bytes stay hosted under the original name -> (`ethrex_mainnet_25368371.bin`, `61eba49b…`) so older `main`s — whose Makefile -> pins that sha256 — keep fetching their own artifact. - -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 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 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 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 that block's cache, rebuilds the .bin -sha256sum "$(make -s print-real-block-fixture)" -``` - -Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_SHA256` and its URL. - -Directly, against any cache file: +The converter reads the block, raw witness preimages, and public network from +the cache. It builds the Amsterdam `NewPayloadRequest`, recovers one public key +per transaction, carries the raw state/code/header witness, and validates the +serialized result with ethrex's native guest before writing it. ```bash cd tooling/ethrex-block-converter cargo run --release -- ``` -Output is deterministic for a given cache file: - -```text -wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110165 bytes): 1 block(s) \ - from mainnet starting at #25368371, 29 transaction(s), 2428684 gas -``` - -Verified at the current ethrex rev: regenerating from the hosted cache reproduces -`0a301731…` byte for byte, and the result passes `test_ethrex_real_block_native` — -which is what proves the hosted cache and the fixture the Makefile expects describe -the same block. (Byte count and digest are both rev-dependent: the `4f658c2b` bump -moved them from 1,110,156 B / `61eba49b…`, the bytes still hosted in the release.) - -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 - -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`** on a PR — also push to main and `workflow_dispatch` | 3 runs, 158.8 s each (~8 min of proving) | -| `bench-abba.yml` | **`/bench-abba [N]`** on a PR | ~72 min at the default 12 pairs (see below) | -| `benchmark-gpu.yml` | **`/bench-gpu [N]`** on a PR | 59.87 s/prove on an RTX 5090 (see below) | -| `scripts/bench_verify.sh` | `scripts/bench_verify.sh ` | ~2.6 min per side, then cached | -| `scripts/perf_diff.sh` | `scripts/perf_diff.sh ` | 5 recordings, so ~13 min of proving | -| `scripts/bench_abba.sh` | `scripts/bench_abba.sh [base] [pairs]` | 2 x 158.8 s per pair | - -This block is what all three scripts prove by default (`WORKLOAD=real`); pass -`WORKLOAD=synthetic` for the N-plain-transfer fixture instead. `/bench-verify` is the -one flow that pins `synthetic`, because it reports a monolithic arm as well as a -continuation one and a real block does not fit monolithically. - -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` on every -invocation, so the digest is re-checked rather than trusted. - -**Every bench flow proves this block.** `/bench` runs it sampled on the shared -runner, against the cached baseline main publishes; `/bench-abba [N]` runs it as -N A/B/B/A pairs on that same runner; `/bench-gpu [N]` runs the same 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. - -**Escalating from `/bench` to `/bench-abba`.** `/bench` resolves about 3%: three -runs of a 158.8 s prove, so it reports 3–10% as unresolved rather than as a -verdict. The paired test resolves a 95% delta of `t* × sd / sqrt(N)`, where `sd` -is the pair-delta standard deviation on the runner. **That sd is not measured -yet.** The columns below bracket it between 1.0% — the GPU box's measured 0.64% -pair sd plus margin — and 2.0%, which is `sqrt(2) ×` this runner's measured 1.43% -single-run CV: - -| pairs | wall | resolves (sd 2.0%) | resolves (sd 1.0%) | -|---|---|---|---| -| 8 | ~50 min | 1.7% | 0.8% | -| **12** | **~72 min** | **1.3%** | **0.6%** | -| 20 | ~1h55m | 0.9% | 0.5% | -| 32 | ~3h | 0.7% | 0.4% | - -12 pairs is the default. Wall is two 158.8 s proves per pair plus ~8 min of -setup. The first real `/bench-abba` run **measures** the sd — it is the `sd` -field of the paired-t line in the result comment — and this table should be -re-pinned to that value once it exists. - -**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`). - -2^22 is also what the CPU runner uses, but the two arrive there for different reasons -and must not be derived from each other: VRAM binds on the GPU path and host RAM on the -CPU one. The workflows pin it on both sides (`GPU_REAL_EPOCH_LOG2` here, -`REAL_BLOCK_EPOCH_LOG2` in `benchmark-pr.yml`, `ABBA_REAL_EPOCH_LOG2` in -`bench-abba.yml`); `bench_abba.sh` and `bench_verify.sh` still *default* to 2^20, as -does the CLI's `DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2`, because 2^22 needs ~32 GiB of -host memory on a CPU build (peak RSS on the calibration box) and would break laptops. -See [Choosing the epoch size](#choosing-the-epoch-size) for the CPU tiers. - -The CPU bench runner is roughly **2.65x** the GPU wall time for the same block: 158.8 s -median against the calibration RTX 5090's 59.87 s, both at epoch 2^22. - -Do not derive one from the other in general: the CPU rate (3.13 s/Mcycle on this -block) 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 **~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 **~32 GiB** at epoch 2^22 on the calibration -box — 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. Host peaks are machine-specific: the bench runner -itself measured **~52 GB** of peak heap for the same block and epoch. See -[Choosing the epoch size](#choosing-the-epoch-size) for the full curve and the other -tiers. The bundle on disk is ~1.15 GB (1.12 GB on the GPU path); a block would have to -be ~1.9x heavier to push it past the 2 GiB (2.147 GB) rkyv offset limit, which needs -`pointer_width_64` to serialize. - -**`/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**: 9.16 ECSM per Mcycle, against a real - block's 2.28. 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, and `/bench-verify` still proves the 20-transfer block so it can report a -monolithic arm as well as a continuation one. - -**The cost is a shared resource.** One runner carries every `/bench`, `/bench-abba` -and `/bench-verify` in the repo, and a `/bench` now occupies it for **~15 min**, on -every comment and every push to main — ~8 min of that is the three proves, the rest is -checkout, a two-sided build, the fixture fetch and the guest ELF. `BENCH_RUNS_REAL` in -`benchmark-pr.yml` is the dial. A run measured **158.8 s** on that runner (median of 3, -2.8% spread, 13 epochs, epoch 2^22), so 3 runs is the right count; the dial is there -for a future block or prover change that takes a run past ~6 min. - -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. +The output starts with the two-byte big-endian schema ID `0x1501`. The current +ethrex 25 stateless schema validates one Amsterdam block at a time. A replay +cache must therefore contain one block, its `slot_number`, its +`block_access_list_hash`, and the raw BAL when the BAL is non-empty. Caches +created before Amsterdam are rejected rather than silently rewriting their +block hash or chain rules. -## Where validation runs +The converter supports empty execution requests, which is the format currently +written by `ethrex-replay` for the public L1 cache. Caches with non-empty +requests are rejected because replay does not persist those request bodies. -The checks themselves are described under [Validation](#validation) below; this is -where each one executes. +## Revision pin -`.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 -packages in the required gate. +The converter, fixture generator, guest, and host tests all pin: -`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` -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. - -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. - -## Adopting a different block - -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.** Five lines: - -```make -ETHREX_REAL_BLOCK_NETWORK := -ETHREX_REAL_BLOCK := -ETHREX_REAL_BLOCK_FIXTURE_URL := -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := -ETHREX_REAL_BLOCK_CACHE_URL / _SHA256 := +```text +https://github.com/lambdaclass/ethrex.git +2cb18b0b95b27a2555d3debffdebc43c9685d6e3 ``` -**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. - -**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 -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. All figures are -measured, never derived from gas — **cycles per gas is not constant** (20.9 for the -current default), so sizing a candidate from its gas mispredicts cost. - -**Current default — main-vintage (merge `fdb92f67`, main @ `9ccdaf2`):** - -These figures were measured on the pre-bump fixture (`61eba49b…`, 1,110,156 B) and are -left as measured rather than restamped. The ethrex `4f658c2b` bump changed the fixture -bytes, so they are a baseline for a workload that no longer exists byte-for-byte. - -Post-bump CPU counterparts, measured ABBA on `vm-benchmarks-1` at the same epoch 2^22: -**45,074,552 cycles** (−11.24%), **142.37 s** CPU prove (−10.87%), **936.7 MB** proof -(−12.22%), peak RSS flat at ~48 GB. The GPU column has no post-bump counterpart yet. - -| 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 | **158.8 s** @ epoch 2^22 (2.65x the GPU wall) | 1.15 GB CPU / 1.12 GB GPU | 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. 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 -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 size -cost:** on one common pre-LTO ELF the three blocks run at 30.8, 29.7 -and 38.2 cycles per gas, so budgeting a candidate from gas alone is off by up to ~29% -— 25453112 and hoodi 1265656 sit within 4% of each other on gas (4.24M vs 4.40M) yet -25453112 costs ~25% fewer cycles (125.9M vs 168.3M). Gas happens to *order* these -three correctly; it does not size them. 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. - -### Choosing the epoch size +Keep these pins together. The SSZ wire format and the guest implementation are +coupled to the ethrex revision. -`--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: +## Real-block fixture -| 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 — it already peaks at ~52 GB here, and 2^23 measured 60 GiB on a roomier box | -| 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 or peak memory off this table for another machine.** This -box took 397.88 s at 2^22 on its branch-vintage ELF; the bench runner takes 158.8 s at -the same epoch on the main-vintage one — a 2.5x gap, against the ~6% the two cycle -counts differ by. The *ratios* between epochs transfer; the wall times do not. Memory -transfers no better: the bench runner peaks at ~52 GB of heap at 2^22 where this box -measured 32.21 GiB RSS. - -### Verifying a repoint - -Run **both**, in this order: +The benchmark fixture is a fetched release artifact, not a build dependency: ```bash -make ethrex-real-block-fixture # fetch + verify the new .bin -make test-ethrex # the block is USABLE on the guest +make ethrex-real-block-fixture ``` -`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 -`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 -`main` while we pin a branch off it, and the type has diverged between the two -before: against the previously pinned rev (`156cb8d6…`) `main` carried an extra -`fee_configs` field and had moved the type from `l1::` to `input::`, so replay's -binary would not deserialize in our guest at all. - -At the currently pinned rev (`4f658c2b…`, rebased onto recent `main`) the -`ProgramInput` definition matches `main`'s again, and that gap has closed for -now. What has not closed: replay resolves rkyv itself from ethrex's `^0.8.10` -rather than our exact `=0.8.16`, `main` has no `lambdavm` feature to build the -guest side against, and the next bump can reopen the type gap without warning. - -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 - -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. 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-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 -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 -post-state-root check, so any divergence from consensus fails here. - -**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`. 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. - -**`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 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`.** 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. - -**`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: +After an ethrex revision bump, regenerate it from the matching cache: ```bash -cd tooling/ethrex-tests && cargo test --release test_ethrex_real_block_vm -- --ignored +make regen-real-block-fixture +sha256sum "$(make -s print-real-block-fixture)" ``` -[replay]: https://github.com/lambdaclass/ethrex-replay +Publish the resulting SSZ artifact and matching Amsterdam cache, then update +their URLs and checksums in the Makefile before enabling the real-block +benchmark. The old release assets are rkyv `ProgramInput` artifacts and are +incompatible with ethrex 25. + +The converter's tests use the Hoodi cache under `caches/` and verify that an +unmapped network is rejected. The checked release cache predates Amsterdam, so +it is intentionally rejected until a cache containing the new payload fields +is published. diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index 87bdfe51d..d7b1c3d1b 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -1,37 +1,38 @@ -//! Convert a real Ethereum block into the rkyv-serialized `ProgramInput` the -//! lambda-vm ethrex guest consumes, from an `ethrex-replay` cache JSON. +//! Convert an `ethrex-replay` cache into the SSZ stateless input consumed by +//! the lambda-vm ethrex guest. //! //! Usage: //! cargo run --release -- +use ethrex_common::constants::{DEFAULT_REQUESTS_HASH, EMPTY_BLOCK_ACCESS_LIST_HASH}; use ethrex_common::types::Block; -use ethrex_common::types::block_execution_witness::{RpcExecutionWitness, decode_witness_headers}; +use ethrex_common::types::block_access_list::BlockAccessList; +use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_common::types::stateless_ssz::{ + Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, + STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, +}; use ethrex_config::networks::Network; use ethrex_crypto::NativeCrypto; -use ethrex_guest_program::l1::ProgramInput; +use ethrex_guest_program::l1::run_stateless_guest; +use libssz::SszEncode; +use libssz_types::{ProgressiveList, SszList, SszVector}; use serde::Deserialize; -/// The subset of `ethrex-replay`'s on-disk cache that a `ProgramInput` needs. +/// The subset of an `ethrex-replay` cache needed by the stateless SSZ input. /// -/// Deliberately deserialized with *our* pinned ethrex types rather than by -/// depending on `ethrex-replay`: it tracks ethrex `main`, we pin a branch off it, -/// and `ProgramInput` has diverged between the two before (it once carried an -/// extra `fee_configs` field and lived at a different module path). At the -/// currently pinned rev the type happens to match `main`'s again, but replay -/// still resolves rkyv itself from ethrex's `^0.8.10` against our exact `=0.8.16`, -/// and nothing stops the type drifting apart on the next bump. This JSON carries -/// only `blocks` + `witness` + `network` as plain serde, so it is the -/// version-tolerant interface between the two regardless. -/// -/// Extra fields in the file (L2 blob data, custom `chain_config`) are ignored. +/// Current replay caches carry the witness as raw RLP preimages. A future cache +/// may carry the Amsterdam block access list beside the block; accepting it here +/// keeps the converter independent of replay's Rust type layout. #[derive(Deserialize)] struct Cache { blocks: Vec, witness: RpcExecutionWitness, network: Network, + #[serde(default, alias = "blockAccessList")] + block_access_list: Option, } -/// Summary of the converted block, for the CLI's one-line report. struct BlockSummary { network: String, first_block_number: u64, @@ -40,56 +41,208 @@ struct BlockSummary { gas_used: u64, } -fn program_input_from_cache( +fn empty_execution_requests() -> ExecutionRequests { + ExecutionRequests { + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), + } +} + +fn ssz_witness( + witness: &RpcExecutionWitness, +) -> Result> { + let state = witness + .state + .iter() + .enumerate() + .map(|(i, bytes)| { + SszList::try_from(bytes.to_vec()) + .map_err(|e| format!("witness state[{i}] is too large: {e:?}")) + }) + .collect::, _>>()?; + let codes = witness + .codes + .iter() + .enumerate() + .map(|(i, bytes)| { + SszList::try_from(bytes.to_vec()) + .map_err(|e| format!("witness codes[{i}] is too large: {e:?}")) + }) + .collect::, _>>()?; + let headers = witness + .headers + .iter() + .enumerate() + .map(|(i, bytes)| { + SszList::try_from(bytes.to_vec()) + .map_err(|e| format!("witness headers[{i}] is too large: {e:?}")) + }) + .collect::, _>>()?; + + Ok(SszExecutionWitness { + state: ProgressiveList::from(state), + codes: ProgressiveList::from(codes), + headers: SszList::try_from(headers) + .map_err(|e| format!("witness has too many headers: {e:?}"))?, + }) +} + +fn stateless_input_from_cache( cache_path: &str, -) -> Result<(ProgramInput, BlockSummary), Box> { +) -> Result<(Vec, 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 { + let Some(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(), - }; - - // 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 cache.blocks.len() != 1 { + return Err(format!( + "cache contains {} blocks; the ethrex 25 stateless guest accepts one block", + cache.blocks.len() + ) + .into()); + } 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)", + "unsupported network `{}`: its chain rules would be guessed, not read", 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`. - // Those headers are now decoded by the caller rather than inside the call, and - // the `Crypto` argument is what recomputes the block hashes the parent lookup - // matches on. `NativeCrypto` is keccak-only here, so it produces the same bytes - // as the `LambdaVmEcsmCrypto` the guest injects (whose host path is also - // software keccak) — the fixture does not depend on which one converts it. - let chain_config = cache.network.get_genesis()?.config; - let decoded_headers = decode_witness_headers(&cache.witness.headers)?; - let witness = cache.witness.into_execution_witness( - chain_config, - summary.first_block_number, - &decoded_headers, - &NativeCrypto, - )?; + let summary = BlockSummary { + network: cache.network.to_string(), + first_block_number: block.header.number, + blocks: 1, + transactions: block.body.transactions.len(), + gas_used: block.header.gas_used, + }; + let header = &block.header; - Ok((ProgramInput::new(cache.blocks, witness), summary)) + // The new wire format is the Amsterdam payload. A pre-Amsterdam replay + // cache has no BAL or slot and cannot be upgraded without replaying the + // block, so reject it instead of changing its block hash or chain rules. + let block_access_list_hash = header + .block_access_list_hash + .ok_or("cache block has no Amsterdam block_access_list_hash")?; + let slot_number = header + .slot_number + .ok_or("cache block has no Amsterdam slot_number")?; + let block_access_list = match cache.block_access_list.as_ref() { + Some(bal) => bal.clone(), + None if block_access_list_hash == *EMPTY_BLOCK_ACCESS_LIST_HASH => BlockAccessList::new(), + None => { + return Err("cache block has a non-empty BAL hash but no raw block access list".into()); + } + }; + if block_access_list.compute_hash(&NativeCrypto) != block_access_list_hash { + return Err("cache block access list does not match block_access_list_hash".into()); + } + + let requests_hash = header + .requests_hash + .ok_or("cache block has no requests_hash")?; + if requests_hash != *DEFAULT_REQUESTS_HASH { + return Err( + "cache contains execution requests, but the replay cache has no request data".into(), + ); + } + let base_fee = header + .base_fee_per_gas + .ok_or("cache block has no base_fee_per_gas")?; + let transactions = block + .body + .transactions + .iter() + .map(|tx| tx.encode_canonical_to_vec().into()) + .collect::>>() + .into(); + let public_keys = block + .body + .transactions + .iter() + .enumerate() + .map(|(i, tx)| { + let key = tx + .public_key(&NativeCrypto) + .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? + .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; + SszVector::try_from(key.to_vec()) + .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) + }) + .collect::, String>>()?; + + let withdrawals = block + .body + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map( + |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: Bytes20(withdrawal.address.0), + amount: withdrawal.amount, + }, + ) + .collect::>() + .into(); + let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec()) + .map_err(|e| format!("logs bloom is not 256 bytes: {e:?}"))?; + let extra_data = SszList::try_from(header.extra_data.to_vec()) + .map_err(|e| format!("extra data exceeds 32 bytes: {e:?}"))?; + let mut base_fee_bytes = [0u8; 32]; + base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); + + let execution_payload = ExecutionPayload { + parent_hash: header.parent_hash.0, + fee_recipient: Bytes20(header.coinbase.0), + state_root: header.state_root.0, + receipts_root: header.receipts_root.0, + logs_bloom, + prev_randao: header.prev_randao.0, + block_number: header.number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data, + base_fee_per_gas: base_fee_bytes, + block_hash: header.compute_block_hash(&NativeCrypto).0, + transactions, + withdrawals, + blob_gas_used: header.blob_gas_used.unwrap_or_default(), + excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), + block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(&block_access_list).into(), + slot_number, + }; + let new_payload_request = NewPayloadRequest { + execution_payload, + versioned_hashes: block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .map(|hash| hash.0) + .collect::>() + .into(), + parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, + execution_requests: empty_execution_requests(), + }; + let input = SszStatelessInput { + new_payload_request, + witness: ssz_witness(&cache.witness)?, + chain_id: cache.network.get_genesis()?.config.chain_id, + public_keys: SszPublicKeys::from(public_keys), + }; + + let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); + input.ssz_append(&mut bytes); + Ok((bytes, summary)) } fn usage_and_exit(program: &str) -> ! { @@ -109,13 +262,15 @@ fn main() -> Result<(), Box> { usage_and_exit(&program); } - let (program_input, summary) = program_input_from_cache(&cache_path)?; - let bytes = rkyv::to_bytes::(&program_input)?; + let (bytes, summary) = stateless_input_from_cache(&cache_path)?; + let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); + if output.len() != 43 || output[32] == 0 { + return Err("converted stateless input failed native validation".into()); + } std::fs::write(&out_path, &bytes)?; println!( - "wrote {out_path} ({} bytes): {} block(s) from {} starting at #{}, \ - {} transaction(s), {} gas", + "wrote {out_path} ({} bytes): {} block(s) from {} starting at #{}, {} transaction(s), {} gas", bytes.len(), summary.blocks, summary.network, @@ -129,109 +284,43 @@ 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"; + const CACHE_MISSING: &str = "caches/cache_hoodi_1265656.json is missing — run `make ethrex-real-block-converter-cache` first"; - /// 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-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 - /// 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-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 - /// 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. - /// - /// 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. - /// - /// `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; - use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; - use std::sync::Arc; - - let (program_input, _) = program_input_from_cache(CACHE).expect(CACHE_MISSING); - execution_program(program_input, Arc::new(LambdaVmEcsmCrypto)).unwrap(); + fn legacy_cache_is_rejected_without_amsterdam_fields() { + let result = stateless_input_from_cache(CACHE); + let Err(error) = result else { + panic!("the checked-in replay cache unexpectedly has Amsterdam fields"); + }; + if !std::path::Path::new(CACHE).exists() { + assert!( + error.to_string().contains("No such file") + || error.to_string().contains("os error"), + "{CACHE_MISSING}: {error}" + ); + } else { + assert!( + error.to_string().contains("Amsterdam"), + "unexpected error: {error}" + ); + } } - /// 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).expect(CACHE_MISSING)).unwrap(); cache["network"] = serde_json::json!("LocalDevnet"); - - let path = std::env::temp_dir().join(format!( - "ethrex_real_block_localdevnet_{}.json", - std::process::id() - )); + let path = + std::env::temp_dir().join(format!("ethrex_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()); + let result = stateless_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"); + let Err(error) = result else { + panic!("LocalDevnet must be rejected"); }; - - 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. - #[test] - fn conversion_is_reproducible() { - use sha2::Digest; - - 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. - let digest: String = sha2::Sha256::digest(&bytes) - .iter() - .map(|b| format!("{b:02x}")) - .collect(); - assert_eq!( - digest, "8b3fb1294a5cd7296452d4a0422b0623c2d243e84bc8a51d3f15bc3899354553", - "fixture bytes changed — regenerate it and update the README checksum", - ); + assert!(error.to_string().contains("unsupported network"), "{error}"); } } diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index 03e77ca4a..bdd48518e 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -975,8 +975,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crossbeam", @@ -1000,8 +1000,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crc32fast", @@ -1034,8 +1034,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ark-bn254", "ark-ec", @@ -1063,9 +1063,12 @@ dependencies = [ "ethrex-common", "ethrex-guest-program", "ethrex-l2-rpc", + "ethrex-rlp", "ethrex-storage", + "ethrex-trie", "hex", - "rkyv", + "libssz", + "libssz-types", "secp256k1", "serde_json", "tokio", @@ -1073,8 +1076,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -1084,6 +1087,10 @@ dependencies = [ "ethrex-rlp", "ethrex-vm", "hex", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "rkyv", "serde", "serde_with", @@ -1092,8 +1099,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -1102,7 +1109,6 @@ dependencies = [ "k256", "lambdaworks-crypto", "rkyv", - "secp256k1", "serde", "serde_with", "thiserror 2.0.18", @@ -1111,8 +1117,8 @@ dependencies = [ [[package]] name = "ethrex-l2-rpc" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "axum", "bytes", @@ -1143,8 +1149,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -1162,8 +1168,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "axum", "ethrex-common", @@ -1178,8 +1184,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "aes", "aes-gcm", @@ -1222,8 +1228,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -1232,8 +1238,8 @@ dependencies = [ [[package]] name = "ethrex-rpc" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "axum", "axum-extra", @@ -1272,8 +1278,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -1294,8 +1300,8 @@ dependencies = [ [[package]] name = "ethrex-storage-rollup" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "async-trait", "bincode", @@ -1310,8 +1316,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -1330,8 +1336,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -2222,18 +2228,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssz" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +checksum = "b4bdd6d63ed811ae164966de20810be780e07de784a4834ccfe6be90480c369e" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +checksum = "aeee1b9ac9200429f7e9830492765445989ea61c3fb9028ad5a96e1dd5f5e913" dependencies = [ "proc-macro2", "quote", @@ -2242,9 +2248,9 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +checksum = "863eca32d1a43e5ec41106a515552efa8307768d37c68d26b7f21ff13cfee1a7" dependencies = [ "libssz", "sha2", @@ -2252,9 +2258,9 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +checksum = "d4231ac301726840a3fe111f11bd4619d3c97ed155cb94c88dbf92b70e04e017" dependencies = [ "libssz", "libssz-merkle", @@ -3696,7 +3702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index daeda8b15..a7335eb15 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -7,18 +7,22 @@ edition = "2024" [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-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-blockchain" } -ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-storage" } -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-common" } -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program" } -ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-l2-rpc" } +# Pinned to the SAME ethrex rev as the guest so generated SSZ inputs match what +# the stateless guest deserializes. +ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-blockchain" } +ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-storage" } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-common" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program" } +ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-l2-rpc" } +ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-rlp" } +# Walking the witness tries: the storage keys are `keccak(slot)`, so recovering a +# genesis-shaped alloc from a witness needs the node types, not just the accessors. +ethrex-trie = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-trie" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -# 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"] } +# Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. +libssz = "=0.3.0" +libssz-types = "=0.3.0" serde_json = "1" bytes = "1" secp256k1 = "0.30" diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 7c9b00e0f..04b734860 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -1,79 +1,84 @@ # ethrex-fixtures -Generates synthetic **ethrex block fixtures** — serialized `ProgramInput` `.bin` -files — for the lambda-vm prover tests and benchmarks. Fully in-memory and -offline (no RPC, no node): it builds a genesis chain, creates a block with a -chosen number of signed ETH-transfer transactions, runs ethrex's stateless -witness generation, and writes the rkyv-encoded `ProgramInput`. +Generates deterministic synthetic ethrex blocks and writes the +schema-prefixed SSZ stateless input consumed by the LambdaVM guest. The tool +builds an in-memory chain, creates signed ETH transfers, generates the raw +execution witness, and validates the resulting input with ethrex's native +stateless guest before writing it. -The ethrex dependency is pinned to the **same revision as the guest** -(`executor/programs/rust/ethrex`), so the produced fixtures deserialize and -execute in the guest. When you bump the guest's ethrex rev, bump the `rev` in -this crate's `Cargo.toml` too and regenerate. - -## Prerequisites -- Rust (stable) and network access (the first build fetches the pinned ethrex - crates). **No RV64 target or sysroot needed** — this is a host tool. - -## How to run +The ethrex revision is pinned to the same commit as the guest. After changing +that pin, regenerate the committed fixtures with `make regen-ethrex-fixtures`. ```bash cd tooling/ethrex-fixtures -cargo run --release -- [mode] +cargo run --release -- [same|recipients|distinct] ``` -- `` — how many ETH transfers to include in the block (`0` = empty - block). -- `` — where to write the `.bin` (relative to this directory). -- `[mode]` — account diversity (optional, default `same`): - - `same` — one funded sender (`RICH_PK`) → one fixed recipient (`0xdeadbeef`). - - `recipients` — one funded sender → N distinct recipients (1 → N fan-out). - - `distinct` — N distinct, genesis-funded senders → N distinct recipients - (N independent 1-1 pairs; senders are deterministic synthetic keys injected - into the genesis allocation). This is what the CI benchmark uses, since the - state-trie witness for many distinct accounts is closer to a real block. - -It prints the output size, the number of transactions, and the mode, e.g.: +`same` uses one funded sender, `recipients` sends to distinct recipients, and +`distinct` uses deterministic funded senders. The standard fixtures are: -``` -wrote ../../executor/tests/ethrex_simple_tx.bin (12745 bytes): block #1 with 1/1 transfer(s) [1 sender -> 1 recipient] +```bash +cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin +cargo run --release -- 1 ../../executor/tests/ethrex_simple_tx.bin +cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin +cargo run --release -- 4 ../../executor/tests/ethrex_bench_4.bin distinct ``` -Output is deterministic for a given `(n_transfers, mode)`. +The generator is host-only; it needs no RV64 target or sysroot. Output is +deterministic for a given transfer count and mode. -## Creating blocks with different numbers of transactions +## The benchmark block: `--bin real_block` -Just change the first argument: +The synthetic blocks above are transfers, which is not what a real block costs: +a mainnet block is keccak- and trie-bound, and a prover change can move the two +numbers in opposite directions. `real_block` produces the benchmark workload +from a real block instead — its own transactions, its own accounts, its own +contract code — rebuilt as an Amsterdam block the guest accepts: ```bash -# empty block (0 transactions) -cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin +cargo run --release --bin real_block -- +# or, with the cache fetched for you: +make regen-real-block-fixture +``` -# 1 transfer -cargo run --release -- 1 ../../executor/tests/ethrex_simple_tx.bin +It reads an ethrex-replay cache, installs the block's pre-state into an +in-memory store **keyed the way the tries are keyed** — `keccak(address)` and +`keccak(slot)`, so no key preimages are needed and every account and slot the +block touches arrives intact — registers a parent header at the block's real +height, replays the transactions in the block's own order through the t8n +entry point, and validates the result through the native guest before writing. -# 10 transfers -cargo run --release -- 10 ../../executor/tests/ethrex_10_transfers.bin +Output for mainnet 25368371 (29 transactions, 2,428,684 gas): -# 50 transfers (custom) -cargo run --release -- 50 /tmp/ethrex_50_transfers.bin +``` +installed 91 accounts / 117 storage slots / 91 codes +rebuilt #25368371 (29/29 txs, 1803938 gas, 12 reverted) ``` -For committed fixtures, prefer `make regen-ethrex-fixtures` from the repo root; -it regenerates the standard fixtures and refreshes -`executor/tests/README.md` checksums. - -> Note: bigger blocks cost ~4M cycles per transfer (software ecrecover -> dominates), so they execute fine but may be too heavy to *prove* on a typical -> machine — e.g. 10 transfers ≈ 42M cycles. - -## Details -- Transactions are plain ETH transfers. In `same`/`recipients` mode they are - signed by a funded dev account from `genesis.json` (well-known load-test key — - not a secret); in `distinct` mode each is signed by its own synthetic key, - funded by injecting an entry into the genesis allocation. Output is - deterministic in all modes. -- Currently only ETH transfers are supported. (ERC20 / contract calls would be - a future extension.) -- Once the upstream LambdaVM-backend ethrex PR merges, this tool can be replaced - by `ethrex-replay custom block` on ethrex `main`. +### Why 12 transactions revert, and why that is not a defect here + +Those transactions were signed with gas limits computed under Osaka. Amsterdam +changes the gas model: cold account access goes from 2600 to 3000, and EIP-8037 +carves a state-gas reservoir out of the transaction's excess gas limit. A +transfer sent with a limit of exactly 21,000 has no excess to carve, so it runs +out of gas. The same rebuild under Osaka rules — `REAL_BLOCK_FORK=osaka`, which +prints the totals and exits without writing, since the guest only decodes the +Amsterdam schema — reverts **2** transactions instead of 12: + +| rules | txs | gas | reverted | +| --- | --- | ---: | ---: | +| Osaka (what the block was built for) | 29/29 | 1,473,522 | 2 | +| Amsterdam | 29/29 | 1,803,938 | 12 | +| mainnet, as mined | 29 | 2,428,684 | — | + +So the gap is the fork, not the reconstruction: a pre-Amsterdam block cannot be +a faithful Amsterdam workload at any level of tooling effort. What this fixture +is, therefore, is a *real-mix* Amsterdam block — real contract code, real +calldata, real signatures (the accelerator counters agree exactly with the +retired fixture at 116 ECSM calls), real trie depth — and not a replay of +mainnet economics. For workloads whose gas limits were computed for Amsterdam, +use the EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the +repository root has both sets measured side by side. + +Measured on the guest ELF at ethrex `2cb18b0b`: 20,360,647 cycles, 2,701 keccak +calls, 116 ECSM calls. diff --git a/tooling/ethrex-fixtures/genesis.json b/tooling/ethrex-fixtures/genesis.json index af3626151..6208bf936 100644 --- a/tooling/ethrex-fixtures/genesis.json +++ b/tooling/ethrex-fixtures/genesis.json @@ -1126,6 +1126,18 @@ "balance": "0xc097ce7bc90715b34b9f1000000000", "nonce": "0x0" }, + "0x0000bff46984e3725691fa540a8c7589300d8282": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd", + "storage": {}, + "balance": "0x0", + "nonce": "0x1" + }, + "0x000064d678505ad48f8ccb093bc65613800e8282": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd", + "storage": {}, + "balance": "0x0", + "nonce": "0x1" + }, "0x4e59b44847b379578588920ca78fbf26c0b4956c": { "code": "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3", "storage": {}, diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs new file mode 100644 index 000000000..b77d96d8a --- /dev/null +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -0,0 +1,512 @@ +//! Rebuild a real mainnet block as an Amsterdam block the stateless guest accepts. +//! +//! The retired benchmark fixture (`ethrex_mainnet_25368371.bin`) is an rkyv +//! `ProgramInput` from before execution-specs #3278. ethrex 25's guest only +//! decodes schema `0x1501` (Amsterdam) and mainnet has no Amsterdam fork yet, so +//! that block cannot be re-serialized: it has no block access list hash and no +//! slot number, and its state carries none of the EIP-8282 predeploys Amsterdam +//! requires. What CAN be reused is its workload — the real transactions and the +//! real accounts they touch. +//! +//! This takes an ethrex-replay cache, seeds a local Amsterdam genesis with the +//! accounts from the block's own execution witness, replays the block's +//! transactions on top, and emits the SSZ stateless input for the resulting +//! block. The output is NOT the mainnet block: the state root, block hash and +//! gas schedule are this chain's. It is the same transaction mix, which is the +//! part no synthetic block reproduces. +//! +//! Usage: +//! cargo run --release --bin real_block -- +//! +//! NOTE: `build_stateless_input` and its helpers are copied from `main.rs` +//! rather than shared. A real PR should lift them into a module; keeping this +//! binary self-contained leaves the committed generator untouched. + +use bytes::Bytes; +use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; +use ethrex_blockchain::{Blockchain, BlockchainOptions}; +use ethrex_common::types::block_access_list::BlockAccessList; +use ethrex_common::types::block_execution_witness::{ + ExecutionWitness, RpcExecutionWitness, amsterdam_chain_config, decode_witness_headers, +}; +use ethrex_common::types::stateless_ssz::{ + Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, + STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, +}; +use ethrex_common::types::{AccountState, Block, ELASTICITY_MULTIPLIER, Genesis}; +use ethrex_common::{Address, H256, U256}; +use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; +use ethrex_guest_program::l1::run_stateless_guest; +use ethrex_rlp::decode::RLPDecode; +use ethrex_rlp::encode::RLPEncode; +use ethrex_storage::{EngineType, Store}; +use ethrex_trie::{Nibbles, Node, NodeRef}; +use libssz::SszEncode; +use libssz_types::{ProgressiveList, SszList, SszVector}; +use std::collections::BTreeMap; + +const GENESIS_JSON: &str = include_str!("../../genesis.json"); +/// Mainnet: the cached transactions are signed for it, so the local chain has to +/// claim the same id or every signature fails. +const CHAIN_ID: u64 = 1; +/// Low enough that every cached transaction clears the block's base fee. The +/// real block's was 0.119 gwei; a transaction rejected for fee reasons would +/// silently shrink the workload. +const GENESIS_BASE_FEE: u64 = 1_000; +const GAS_CEIL: u64 = 60_000_000; + +fn empty_execution_requests() -> ExecutionRequests { + ExecutionRequests { + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), + } +} + +fn build_stateless_input( + block: &Block, + witness: &RpcExecutionWitness, + block_access_list: Option<&BlockAccessList>, + chain_id: u64, +) -> Result, Box> { + let header = &block.header; + let bal = block_access_list.ok_or("Amsterdam fixture has no block access list")?; + let block_access_list_hash = header + .block_access_list_hash + .ok_or("Amsterdam fixture header has no block_access_list_hash")?; + if bal.compute_hash(&NativeCrypto) != block_access_list_hash { + return Err("fixture BAL does not match block_access_list_hash".into()); + } + let slot_number = header + .slot_number + .ok_or("Amsterdam fixture header has no slot_number")?; + + let transactions = block + .body + .transactions + .iter() + .map(|tx| tx.encode_canonical_to_vec().into()) + .collect::>>() + .into(); + let public_keys = block + .body + .transactions + .iter() + .enumerate() + .map(|(i, tx)| { + let key = tx + .public_key(&NativeCrypto) + .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? + .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; + SszVector::try_from(key.to_vec()) + .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) + }) + .collect::, String>>()?; + let withdrawals = block + .body + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map( + |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: Bytes20(withdrawal.address.0), + amount: withdrawal.amount, + }, + ) + .collect::>() + .into(); + let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec())?; + let extra_data = SszList::try_from(header.extra_data.to_vec())?; + let base_fee = header.base_fee_per_gas.ok_or("fixture has no base fee")?; + let mut base_fee_bytes = [0u8; 32]; + base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); + + let execution_payload = ExecutionPayload { + parent_hash: header.parent_hash.0, + fee_recipient: Bytes20(header.coinbase.0), + state_root: header.state_root.0, + receipts_root: header.receipts_root.0, + logs_bloom, + prev_randao: header.prev_randao.0, + block_number: header.number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data, + base_fee_per_gas: base_fee_bytes, + block_hash: header.compute_block_hash(&NativeCrypto).0, + transactions, + withdrawals, + blob_gas_used: header.blob_gas_used.unwrap_or_default(), + excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), + block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(bal).into(), + slot_number, + }; + let ssz_witness = SszExecutionWitness { + state: ProgressiveList::from( + witness + .state + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + codes: ProgressiveList::from( + witness + .codes + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + headers: SszList::try_from( + witness + .headers + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + )?, + }; + let input = SszStatelessInput { + new_payload_request: NewPayloadRequest { + execution_payload, + versioned_hashes: block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .map(|hash| hash.0) + .collect::>() + .into(), + parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, + execution_requests: empty_execution_requests(), + }, + witness: ssz_witness, + chain_id, + public_keys: SszPublicKeys::from(public_keys), + }; + let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); + input.ssz_append(&mut bytes); + Ok(bytes) +} + +/// Walk an embedded witness trie and return every `(32-byte key, value)` leaf it +/// still holds. Pruned subtrees appear as `NodeRef::Hash` and are skipped: they +/// are the proof siblings, which no transaction in the block reads. +fn collect_leaves(node: &Node, path: Nibbles, out: &mut Vec<(H256, Vec)>) { + let descend = |child: &NodeRef, path: Nibbles, out: &mut Vec<(H256, Vec)>| { + if let NodeRef::Node(child, _) = child { + collect_leaves(child, path, out); + } + }; + match node { + Node::Branch(branch) => { + for (i, child) in branch.choices.iter().enumerate() { + descend(child, path.append_new(i as u8), out); + } + } + Node::Extension(ext) => descend(&ext.child, path.concat(&ext.prefix), out), + Node::Leaf(leaf) => { + let bytes = path.concat(&leaf.partial).to_bytes(); + if bytes.len() == 32 { + out.push((H256::from_slice(&bytes), leaf.value.clone())); + } + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut args = std::env::args(); + let program = args.next().unwrap_or_else(|| "real_block".into()); + let (Some(cache_path), Some(out_path)) = (args.next(), args.next()) else { + eprintln!("usage: {program} "); + std::process::exit(2); + }; + + // --- 1. cache -> real block + its execution witness -------------------- + let cache: serde_json::Value = serde_json::from_slice(&std::fs::read(&cache_path)?)?; + let blocks = cache + .get("blocks") + .and_then(|b| b.as_array()) + .ok_or("cache has no `blocks` array")?; + if blocks.len() != 1 { + return Err(format!("cache holds {} blocks; expected exactly 1", blocks.len()).into()); + } + let real_block: Block = serde_json::from_value(blocks[0].clone())?; + let rpc_witness: RpcExecutionWitness = serde_json::from_value( + cache + .get("witness") + .ok_or("cache has no `witness`")? + .clone(), + )?; + + let decoded_headers = decode_witness_headers(&rpc_witness.headers) + .map_err(|e| format!("decode witness headers: {e:?}"))?; + let witness: ExecutionWitness = rpc_witness + .clone() + .into_execution_witness( + amsterdam_chain_config(CHAIN_ID), + real_block.header.number, + &decoded_headers, + &NativeCrypto, + ) + .map_err(|e| format!("into_execution_witness: {e:?}"))?; + + let codes_by_hash: std::collections::HashMap> = witness + .codes + .iter() + .map(|code| (H256(NativeCrypto.keccak256(code)), code.clone())) + .collect(); + // --- 2. a local Amsterdam chain to host the block ---------------------- + // The committed genesis carries the system contracts and the two EIP-8282 + // predeploys Amsterdam needs; only the chain id and fee floor change. The + // block's own accounts do NOT go in the alloc — they are installed into the + // tries below, which needs no key preimages. + let mut genesis: Genesis = serde_json::from_str(GENESIS_JSON)?; + let blob_schedule = genesis.config.blob_schedule; + genesis.config = amsterdam_chain_config(CHAIN_ID); + genesis.config.blob_schedule = blob_schedule; + // `REAL_BLOCK_FORK=osaka` executes the same block under the rules it was + // built for. The guest cannot consume the result (it only decodes the + // Amsterdam schema), so this exists purely to attribute a gas difference to + // the fork rather than to this tool's reconstruction. + let osaka_probe = std::env::var("REAL_BLOCK_FORK").as_deref() == Ok("osaka"); + if osaka_probe { + genesis.config.amsterdam_time = None; + } + genesis.gas_limit = GAS_CEIL; + genesis.base_fee_per_gas = Some(GENESIS_BASE_FEE); + genesis.timestamp = real_block.header.timestamp.saturating_sub(12); + + let mut accounts = Vec::new(); + if let Some(root) = &witness.state_trie_root { + collect_leaves(root, Nibbles::default(), &mut accounts); + } + + let mut store = Store::new(".ethrex-real-block-tmp", EngineType::InMemory)?; + store.add_initial_state(genesis).await?; + let genesis_header = store + .get_block_header(store.get_latest_block_number()?)? + .ok_or("missing genesis header")?; + + // Install the block's own pre-state into the store's tries, keyed the way the + // tries themselves are keyed: `keccak(address)` and `keccak(slot)`. That is + // what makes this faithful — a `Genesis.alloc` is keyed by the preimages, and + // the witness cannot give those back (they are hashes; the replay cache's + // `keys` field carries only the few the RPC returned, and upstream is removing + // it). Writing the hashed keys directly needs no preimages at all, so every + // account and every storage slot the block touches arrives intact. + let genesis_root = genesis_header.state_root; + // The layered opener's `put_batch` is `unimplemented!()` — the writable path is + // the direct one, which talks to the storage backend without the reorg overlay. + let mut state_trie = store.open_direct_state_trie(genesis_root)?; + let mut installed_accounts = 0usize; + let mut installed_slots = 0usize; + let mut installed_codes = 0usize; + for (hashed_address, encoded) in &accounts { + let mut account = AccountState::decode(encoded) + .map_err(|e| format!("decode account {hashed_address:#x}: {e:?}"))?; + if let Some(storage_root) = witness.storage_trie_roots.get(hashed_address) { + let mut leaves = Vec::new(); + collect_leaves(storage_root, Nibbles::default(), &mut leaves); + let mut storage_trie = store.open_direct_storage_trie( + *hashed_address, + *ethrex_common::constants::EMPTY_TRIE_HASH, + )?; + for (hashed_slot, value) in &leaves { + storage_trie.insert(hashed_slot.0.to_vec(), value.clone())?; + } + installed_slots += leaves.len(); + account.storage_root = storage_trie.hash(&NativeCrypto)?; + } + if let Some(code) = codes_by_hash.get(&account.code_hash) { + store + .add_account_code(ethrex_common::types::Code::from_bytecode( + Bytes::from(code.clone()), + &NativeCrypto, + )) + .await?; + installed_codes += 1; + } + state_trie.insert(hashed_address.0.to_vec(), account.encode_to_vec())?; + installed_accounts += 1; + } + let installed_root = state_trie.hash(&NativeCrypto)?; + + // Build at the real height, not at #1. The seeded accounts hold the block's + // own pre-state, but a contract that stores a block number and compares it + // against `block.number` reads nonsense on a chain that just started — and + // several of the block's transactions do exactly that. So register a parent + // header at the real parent's height carrying the genesis state root (which + // is the seeded state), and build on top of it. The guest only ever sees + // this parent and the block itself, and those two are contiguous. + let mut head = genesis_header.clone(); + head.hash = Default::default(); + head.number = real_block.header.number - 1; + head.parent_hash = genesis_header.hash(); + head.timestamp = real_block.header.timestamp.saturating_sub(12); + head.gas_limit = GAS_CEIL; + head.base_fee_per_gas = Some(GENESIS_BASE_FEE); + head.state_root = installed_root; + let head_hash = head.hash(); + let head_number = head.number; + store.add_block_header(head_hash, head.clone()).await?; + store + .add_block_body(head_hash, ethrex_common::types::BlockBody::empty()) + .await?; + store.add_block_number(head_hash, head_number).await?; + store + .forkchoice_update( + vec![(head_number, head_hash)], + head_number, + head_hash, + None, + None, + ) + .await?; + // Real blocks carry builder/MEV transactions with a zero tip cap, which the + // default mempool floor rejects. Dropping them would silently shrink the + // workload this fixture exists to reproduce. + let blockchain = Blockchain::new( + store.clone(), + BlockchainOptions { + min_tip_wei: 0, + ..BlockchainOptions::default() + }, + ); + + // --- 3. replay the real transactions, in the block's own order ---------- + // Not through the mempool: `build_payload` would re-sort by effective tip and + // an arbitrage transaction that ran second on mainnet reverts when it runs + // first. The t8n entry point takes an explicit, ordered list, reports what it + // could not apply instead of aborting, and seeds BLOCKHASH with the ancestor + // hashes the witness carries. + let total_txs = real_block.body.transactions.len(); + let payload_args = BuildPayloadArgs { + parent: head_hash, + timestamp: head.timestamp + 12, + fee_recipient: real_block.header.coinbase, + random: real_block.header.prev_randao, + withdrawals: Some(vec![]), + beacon_root: Some(H256::zero()), + slot_number: Some(1), + version: 4, + elasticity_multiplier: ELASTICITY_MULTIPLIER, + gas_ceil: GAS_CEIL, + }; + let skeleton = create_payload(&payload_args, &store, Bytes::new())?; + let mut block_hash_cache = BTreeMap::new(); + block_hash_cache.insert(head_number, head_hash); + let (result, rejected, t8n_error) = blockchain.build_payload_t8n( + skeleton, + real_block.body.transactions.clone(), + block_hash_cache, + false, + )?; + if let Some(error) = t8n_error { + return Err(format!("payload build reported: {error}").into()); + } + + // Amsterdam always emits one `EncodedRequests` per request type (a lone type + // byte when the list is empty), so the count is not the signal — a non-empty + // payload is. The SSZ input below declares no requests, which only matches + // the header's `requests_hash` while every list is in fact empty. + let non_empty_requests = result + .requests + .iter() + .filter(|encoded| !encoded.is_empty()) + .count(); + if non_empty_requests > 0 { + return Err(format!( + "the rebuilt block produced {non_empty_requests} non-empty EIP-7685 request \ + list(s); the SSZ input this tool writes declares none" + ) + .into()); + } + if osaka_probe { + let reverted = result.receipts.iter().filter(|r| !r.succeeded).count(); + println!( + "osaka probe #{} ({}/{} txs, {} gas, {reverted} reverted) vs real {} gas", + result.payload.header.number, + result.payload.body.transactions.len(), + total_txs, + result.payload.header.gas_used, + real_block.header.gas_used, + ); + return Ok(()); + } + + let block = result.payload; + let included = block.body.transactions.len(); + + // --- 4. witness -> SSZ -> native validation ---------------------------- + let witness = blockchain + .generate_witness_for_blocks(std::slice::from_ref(&block)) + .await?; + let witness: RpcExecutionWitness = witness.try_into()?; + let bytes = build_stateless_input( + &block, + &witness, + result.block_access_list.as_ref(), + CHAIN_ID, + )?; + let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); + if output.len() != 43 || output[32] == 0 { + return Err("rebuilt block failed native stateless validation".into()); + } + std::fs::write(&out_path, &bytes)?; + + // --- 5. report --------------------------------------------------------- + let reverted = result.receipts.iter().filter(|r| !r.succeeded).count(); + let mut per_tx = String::new(); + let mut prev_cumulative = 0u64; + for (i, receipt) in result.receipts.iter().enumerate() { + let gas = receipt.cumulative_gas_used.saturating_sub(prev_cumulative); + prev_cumulative = receipt.cumulative_gas_used; + let to = match block.body.transactions.get(i).map(|tx| tx.to()) { + Some(ethrex_common::types::TxKind::Call(address)) => format!("{address:#x}"), + Some(ethrex_common::types::TxKind::Create) => "create".to_string(), + None => "?".to_string(), + }; + per_tx.push_str(&format!( + " tx {i:>2}: {:>9} gas {:<8} -> {to}\n", + gas, + if receipt.succeeded { "ok" } else { "REVERTED" } + )); + } + println!( + "real block #{} ({} txs, {} gas)", + real_block.header.number, total_txs, real_block.header.gas_used + ); + println!( + "witness {} account leaves / {} codes / {} trie nodes", + accounts.len(), + rpc_witness.codes.len(), + rpc_witness.state.len() + ); + println!( + "installed {installed_accounts} accounts / {installed_slots} storage slots / \ + {installed_codes} codes, state root {installed_root:#x}" + ); + println!( + "rebuilt #{} ({included}/{total_txs} txs, {} gas, {reverted} reverted)", + block.header.number, block.header.gas_used + ); + print!("{per_tx}"); + if !rejected.is_empty() { + println!("rejected ({} of {total_txs}):", rejected.len()); + for entry in &rejected { + println!(" {entry:?}"); + } + } + println!( + "gas vs real {:+.2}%", + (block.header.gas_used as f64 / real_block.header.gas_used as f64 - 1.0) * 100.0 + ); + println!("wrote {out_path} ({} bytes)", bytes.len()); + Ok(()) +} diff --git a/tooling/ethrex-fixtures/src/main.rs b/tooling/ethrex-fixtures/src/main.rs index 5cb60acae..4875fde77 100644 --- a/tooling/ethrex-fixtures/src/main.rs +++ b/tooling/ethrex-fixtures/src/main.rs @@ -1,4 +1,4 @@ -//! Generate synthetic ethrex block fixtures (serialized `ProgramInput`) for the +//! Generate synthetic ethrex block fixtures (SSZ stateless inputs) for the //! lambda-vm prover/benchmarks — in-memory, offline, deterministic. //! //! Usage: @@ -18,19 +18,28 @@ //! the LambdaVM-backend ethrex PR lands on ethrex `main` and fixtures are //! generated via `ethrex-replay custom block` instead. //! -//! Pinned to the same ethrex rev as the guest, so the rkyv `ProgramInput` -//! layout matches what the guest deserializes. +//! Pinned to the same ethrex rev as the guest, so the SSZ layout matches what +//! the guest deserializes. use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; use ethrex_blockchain::{Blockchain, BlockchainOptions}; +use ethrex_common::types::block_access_list::BlockAccessList; +use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_common::types::stateless_ssz::{ + Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, + STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, +}; use ethrex_common::types::{ EIP1559Transaction, ELASTICITY_MULTIPLIER, Genesis, GenesisAccount, Transaction, TxKind, }; use ethrex_common::{Address, H256, U256}; -use ethrex_guest_program::l1::ProgramInput; +use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_guest_program::l1::run_stateless_guest; use ethrex_l2_rpc::signer::{LocalSigner, Signable, Signer}; use ethrex_storage::{EngineType, Store}; +use libssz::SszEncode; +use libssz_types::{ProgressiveList, SszList, SszVector}; use secp256k1::SecretKey; /// Well-known load-test rich account (funded in genesis.json). Key is public @@ -76,6 +85,144 @@ fn rich_signer() -> Result> { Ok(LocalSigner::new(SecretKey::from_slice(&hex::decode(RICH_PK)?)?).into()) } +fn empty_execution_requests() -> ExecutionRequests { + ExecutionRequests { + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), + } +} + +fn build_stateless_input( + block: ðrex_common::types::Block, + witness: &RpcExecutionWitness, + block_access_list: Option<&BlockAccessList>, + chain_id: u64, +) -> Result, Box> { + let header = &block.header; + let bal = block_access_list.ok_or("Amsterdam fixture has no block access list")?; + let block_access_list_hash = header + .block_access_list_hash + .ok_or("Amsterdam fixture header has no block_access_list_hash")?; + if bal.compute_hash(&NativeCrypto) != block_access_list_hash { + return Err("fixture BAL does not match block_access_list_hash".into()); + } + let slot_number = header + .slot_number + .ok_or("Amsterdam fixture header has no slot_number")?; + + let transactions = block + .body + .transactions + .iter() + .map(|tx| tx.encode_canonical_to_vec().into()) + .collect::>>() + .into(); + let public_keys = block + .body + .transactions + .iter() + .enumerate() + .map(|(i, tx)| { + let key = tx + .public_key(&NativeCrypto) + .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? + .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; + SszVector::try_from(key.to_vec()) + .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) + }) + .collect::, String>>()?; + let withdrawals = block + .body + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map( + |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: Bytes20(withdrawal.address.0), + amount: withdrawal.amount, + }, + ) + .collect::>() + .into(); + let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec())?; + let extra_data = SszList::try_from(header.extra_data.to_vec())?; + let base_fee = header.base_fee_per_gas.ok_or("fixture has no base fee")?; + let mut base_fee_bytes = [0u8; 32]; + base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); + + let execution_payload = ExecutionPayload { + parent_hash: header.parent_hash.0, + fee_recipient: Bytes20(header.coinbase.0), + state_root: header.state_root.0, + receipts_root: header.receipts_root.0, + logs_bloom, + prev_randao: header.prev_randao.0, + block_number: header.number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data, + base_fee_per_gas: base_fee_bytes, + block_hash: header.compute_block_hash(&NativeCrypto).0, + transactions, + withdrawals, + blob_gas_used: header.blob_gas_used.unwrap_or_default(), + excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), + block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(bal).into(), + slot_number, + }; + let ssz_witness = SszExecutionWitness { + state: ProgressiveList::from( + witness + .state + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + codes: ProgressiveList::from( + witness + .codes + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + headers: SszList::try_from( + witness + .headers + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + )?, + }; + let input = SszStatelessInput { + new_payload_request: NewPayloadRequest { + execution_payload, + versioned_hashes: block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .map(|hash| hash.0) + .collect::>() + .into(), + parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, + execution_requests: empty_execution_requests(), + }, + witness: ssz_witness, + chain_id, + public_keys: SszPublicKeys::from(public_keys), + }; + let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); + input.ssz_append(&mut bytes); + Ok(bytes) +} + /// Distinct recipient address for tx index `i` (fresh account, no funding needed). fn recipient_for(i: u64) -> Address { Address::from_low_u64_be(0xdead_0000_0000u64 + i) @@ -107,6 +254,11 @@ async fn main() -> Result<(), Box> { // --- 1. genesis -> in-memory store ------------------------------------- let mut genesis: Genesis = serde_json::from_str(GENESIS_JSON)?; + // The stateless guest consumes the Amsterdam SSZ schema. Keep the local + // chain's fork schedule aligned with that schema while preserving the + // fixture's deterministic execution rules. + genesis.config.amsterdam_time = Some(0); + genesis.config.blob_schedule.amsterdam = Some(genesis.config.blob_schedule.bpo2); // For `distinct`, fund each synthetic sender in genesis so its tx is valid. if mode == Mode::Distinct { @@ -127,7 +279,7 @@ async fn main() -> Result<(), Box> { let mut store = Store::new(".ethrex-fixtures-tmp", EngineType::InMemory)?; store.add_initial_state(genesis).await?; - let head_number = store.get_latest_block_number().await?; + let head_number = store.get_latest_block_number()?; let head = store .get_block_header(head_number)? .ok_or("missing genesis header")?; @@ -187,14 +339,15 @@ async fn main() -> Result<(), Box> { random: H256::zero(), withdrawals: Some(vec![]), beacon_root: Some(H256::zero()), - slot_number: None, - version: 3, + slot_number: Some(1), + version: 4, elasticity_multiplier: ELASTICITY_MULTIPLIER, gas_ceil: 60_000_000, }; let skeleton = create_payload(&payload_args, &store, Bytes::new())?; let result = blockchain.build_payload(skeleton)?; let block = result.payload; + let block_access_list = result.block_access_list; let included = block.body.transactions.len(); assert_eq!( included as u64, n_transfers, @@ -202,12 +355,16 @@ async fn main() -> Result<(), Box> { (check gas limit / account balance / nonces)" ); - // --- 4. stateless witness -> ProgramInput -> rkyv ---------------------- + // --- 4. stateless witness -> SSZ --------------------------------------- let witness = blockchain .generate_witness_for_blocks(std::slice::from_ref(&block)) .await?; - let program_input = ProgramInput::new(vec![block], witness); - let bytes = rkyv::to_bytes::(&program_input)?; + let witness: RpcExecutionWitness = witness.try_into()?; + let bytes = build_stateless_input(&block, &witness, block_access_list.as_ref(), chain_id)?; + let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); + if output.len() != 43 || output[32] == 0 { + return Err("generated stateless fixture failed native validation".into()); + } std::fs::write(&out_path, &bytes)?; let mode_label = match mode { diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 0a86228f1..62e8cd38c 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -693,8 +693,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "crc32fast", @@ -714,7 +714,6 @@ dependencies = [ "libssz-types", "lru", "once_cell", - "rayon", "rkyv", "rustc-hash", "secp256k1", @@ -727,8 +726,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "ark-bn254", "ark-ec", @@ -748,8 +747,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -759,6 +758,10 @@ dependencies = [ "ethrex-rlp", "ethrex-vm", "hex", + "libssz", + "libssz-derive", + "libssz-merkle", + "libssz-types", "rkyv", "serde", "serde_with", @@ -767,8 +770,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -777,7 +780,6 @@ dependencies = [ "k256", "lambdaworks-crypto", "rkyv", - "secp256k1", "serde", "serde_with", "thiserror 2.0.18", @@ -786,8 +788,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -796,7 +798,6 @@ dependencies = [ "ethrex-rlp", "libssz", "malachite", - "rayon", "rustc-hash", "serde", "strum", @@ -805,8 +806,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "ethereum-types", @@ -819,13 +820,13 @@ version = "0.1.0" dependencies = [ "ethrex-guest-program", "executor", - "rkyv", + "libssz", ] [[package]] name = "ethrex-trie" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "anyhow", "bytes", @@ -844,8 +845,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "23.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=797df5540c7d35cafd69b6971a74b2a49c67d1dd#797df5540c7d35cafd69b6971a74b2a49c67d1dd" +version = "25.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" dependencies = [ "bytes", "derive_more", @@ -854,7 +855,6 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", - "rayon", "rustc-hash", "serde", "thiserror 2.0.18", @@ -1282,18 +1282,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssz" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498c0482bba87d2647ea4601ea76cf2b498065e3958798a88f49274f3ced5e9" +checksum = "b4bdd6d63ed811ae164966de20810be780e07de784a4834ccfe6be90480c369e" dependencies = [ "smallvec", ] [[package]] name = "libssz-derive" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ddfb5c969c28a4a54043e630f80c723352637bd1020f256ee3ac7a8814922b" +checksum = "aeee1b9ac9200429f7e9830492765445989ea61c3fb9028ad5a96e1dd5f5e913" dependencies = [ "proc-macro2", "quote", @@ -1302,9 +1302,9 @@ dependencies = [ [[package]] name = "libssz-merkle" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c6d6d5ce5d79bba66bc98c99869eedffedf7f14f0aa0915f1a62802650bdf6" +checksum = "863eca32d1a43e5ec41106a515552efa8307768d37c68d26b7f21ff13cfee1a7" dependencies = [ "libssz", "sha2", @@ -1312,9 +1312,9 @@ dependencies = [ [[package]] name = "libssz-types" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747273ab2d923e82ed147091fe0fb3e602dd2012c872cdad5efe69e27c3b4099" +checksum = "d4231ac301726840a3fe111f11bd4619d3c97ed155cb94c88dbf92b70e04e017" dependencies = [ "libssz", "libssz-merkle", diff --git a/tooling/ethrex-tests/Cargo.toml b/tooling/ethrex-tests/Cargo.toml index 3c89ea8f7..484f80b76 100644 --- a/tooling/ethrex-tests/Cargo.toml +++ b/tooling/ethrex-tests/Cargo.toml @@ -3,20 +3,16 @@ name = "ethrex-tests" version = "0.1.0" edition = "2024" -# Detached workspace: ethrex pins rkyv with the `unaligned` feature (a global -# archived-layout switch), which must never feature-unify with the main -# workspace's aligned rkyv proof format. See tests/ethrex.rs. +# Detached workspace: the ethrex host reference has its own dependency graph, +# separate from the main workspace. See tests/ethrex.rs. [workspace] [dev-dependencies] executor = { path = "../../executor" } -# Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch) -# so the native reference reads the same ProgramInput rkyv layout. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "797df5540c7d35cafd69b6971a74b2a49c67d1dd", package = "ethrex-guest-program" } -# Exact pin: the fixture writer and the guest/executor readers must agree on the -# rkyv layout. Keep this in sync with tooling/ethrex-fixtures and -# executor/programs/rust/ethrex/Cargo.toml. -rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } +# Pinned to the SAME ethrex rev as the guest so the native reference reads the +# same SSZ stateless input and output. +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program" } +libssz = "=0.3.0" # Match the root workspace's optimized dev profile: these tests execute the VM. [profile.dev] diff --git a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs new file mode 100644 index 000000000..9bf6743f0 --- /dev/null +++ b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs @@ -0,0 +1,81 @@ +//! Throwaway probe (untracked): do the upstream EEST Amsterdam benchmark +//! fixtures execute on the guest this branch pins? +//! +//! Inputs are `statelessInputBytes` lifted verbatim out of +//! `tests-zkevm-benchmark@v0.8.2` blockchain tests, so nothing is rebuilt host +//! side. Run with: +//! cargo test --manifest-path tooling/ethrex-tests/Cargo.toml --release \ +//! --test eest_amsterdam_probe -- --nocapture + +use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_guest_program::l1::run_stateless_guest; +use std::sync::Arc; + +const BIN_DIR: &str = "../../eest_amsterdam_bins"; + +#[test] +fn eest_amsterdam_fixtures_validate_natively() { + // The .bin files are downloaded/generated artifacts, not committed: a checkout + // without them skips rather than fails, so this probe can live in the tree. + if !std::path::Path::new(BIN_DIR).is_dir() { + println!("{BIN_DIR} absent — skipping"); + return; + } + let mut entries: Vec<_> = std::fs::read_dir(BIN_DIR) + .expect("bin dir") + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().is_some_and(|x| x == "bin")) + .collect(); + entries.sort(); + assert!(!entries.is_empty(), "no .bin found in {BIN_DIR}"); + + for path in entries { + let inputs = std::fs::read(&path).expect("read fixture"); + let schema_id = u16::from_be_bytes([inputs[0], inputs[1]]); + let started = std::time::Instant::now(); + let output = run_stateless_guest(&inputs, Arc::new(NativeCrypto)); + let elapsed = started.elapsed(); + let name = path.file_name().unwrap().to_string_lossy(); + println!( + "{:<60} in={:>9} B schema={:#06x} out={} B ok={} chain_id={} native={:?}", + &name[..name.len().min(60)], + inputs.len(), + schema_id, + output.len(), + output.get(32).copied().unwrap_or(255), + u64::from_le_bytes(output[33..41].try_into().unwrap()), + elapsed, + ); + assert_eq!(output.len(), 43, "{name}: unexpected output length"); + assert_eq!(output[32], 1, "{name}: native stateless validation failed"); + } +} + +/// What the retired real-block fixture does on this branch's guest: the rkyv +/// archive's first two bytes are not the Amsterdam schema id, so the input is +/// rejected and the guest commits an all-zero result. It does NOT error out — +/// which is exactly why a stale fixture would silently benchmark nothing. +/// +/// Reads a copy kept outside `executor/tests`, since that directory now holds the +/// regenerated (valid) fixture for the same block. Skips when the copy is absent. +#[test] +fn legacy_rkyv_real_block_is_rejected_silently() { + let path = format!("{BIN_DIR}/legacy_rkyv_real_block.bin"); + if !std::path::Path::new(&path).is_file() { + println!("{path} absent — skipping"); + return; + } + let inputs = std::fs::read(&path).expect("legacy fixture"); + let output = run_stateless_guest(&inputs, Arc::new(NativeCrypto)); + println!( + "legacy fixture: {} B, first two bytes {:#04x}{:02x}, output {} B, ok={}, all_zero={}", + inputs.len(), + inputs[0], + inputs[1], + output.len(), + output[32], + output.iter().all(|b| *b == 0), + ); + assert_eq!(output.len(), 43); + assert_eq!(output[32], 0, "legacy input must not validate"); +} diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 9ad1ee786..95943ad99 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -1,22 +1,19 @@ -//! Host-reference execution tests for the ethrex guest, relocated out of the -//! `executor` test suite: `ethrex-guest-program` pins rkyv with the -//! `unaligned` feature (a global archived-layout switch), which would -//! feature-unify with the main workspace's aligned rkyv and silently change -//! the proof wire format. This crate is a detached workspace so the two rkyv -//! configurations never meet. +//! Host-reference and VM execution tests for the ethrex stateless guest. //! //! Fixtures are generated by `tooling/ethrex-fixtures`; the guest ELF comes //! from `make compile-programs`. See `executor/tests/README.md`. +use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_guest_program::l1::run_stateless_guest; use executor::elf::Elf; -use executor::vm::execution::Executor; -use executor::vm::execution::ReturnValues; +use executor::vm::execution::{Executor, ExecutorError, ReturnValues}; +use std::sync::Arc; fn run_program_without_expect( elf_path: &str, private_inputs: Vec, -) -> Result { - println!("Testing {}", elf_path); +) -> Result { + println!("Testing {elf_path}"); let elf_data = std::fs::read(elf_path).unwrap(); let program = Elf::load(&elf_data).unwrap(); println!("Program entry: 0x{:016x}", program.entry_point); @@ -25,118 +22,57 @@ fn run_program_without_expect( executor.finish() } -fn run_program_and_check_public_output( - elf_path: &str, - expected_output: Vec, - private_inputs: Vec, -) { - let result = - run_program_without_expect(elf_path, private_inputs).expect("Failed to run program"); +fn native_output(inputs: &[u8]) -> Vec { + let output = run_stateless_guest(inputs, Arc::new(NativeCrypto)); + assert_eq!(output.len(), 43, "unexpected stateless output length"); + assert_eq!(output[32], 1, "native stateless validation failed"); + output +} - assert_eq!(result.memory_values, expected_output); +fn run_fixture(name: &str) { + let inputs = std::fs::read(format!("{FIXTURES_DIR}/{name}")).unwrap(); + let output = native_output(&inputs); + let result = run_program_without_expect(ELF_PATH, inputs).expect("failed to run guest ELF"); + assert_eq!(result.memory_values, output); } 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. -/// (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"] +#[ignore = "heavier synthetic block; run in the dedicated ignored CI step"] #[test] fn test_ethrex() { - use ethrex_guest_program::crypto::NativeCrypto; - use ethrex_guest_program::l1::{ProgramInput, execution_program}; - use rkyv::rancor::Error; - use std::fs; - use std::sync::Arc; - let inputs = fs::read(format!("{FIXTURES_DIR}/ethrex_10_transfers.bin")).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); + run_fixture("ethrex_10_transfers.bin"); } -/// Executes a stateless ethrex block containing a single (plain ETH transfer) -/// transaction. Execution only — no proving — against the ethrex guest ELF -/// built from the same pinned ethrex revision as the native reference. #[test] fn test_ethrex_simple_tx() { - 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}/ethrex_simple_tx.bin")).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); + run_fixture("ethrex_simple_tx.bin"); } -/// Executes a stateless ethrex block with NO transactions (empty block). -/// Execution only — no proving. Pins the committed `ethrex_empty_block.bin` -/// fixture so its rkyv `ProgramInput` layout (the 0-tx edge case) is -/// exercised and stays consistent with the guest across ethrex rev bumps. #[test] fn test_ethrex_empty_block() { - 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}/ethrex_empty_block.bin")).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); + run_fixture("ethrex_empty_block.bin"); } const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin"; -/// Host-only acceptance gate for the real-block fixture that -/// `make ethrex-real-block-fixture` fetches and checksums (produced offline by -/// `tooling/ethrex-block-converter`): 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 published rkyv -/// bytes deserialize and execute — which is why it reads the `.bin` rather -/// 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-block-converter`'s parity test does NOT cover 0x0a — it links -/// `c-kzg` transitively via `ethrex-config`.) +/// Host-only acceptance gate for the real SSZ stateless fixture produced by +/// `tooling/ethrex-block-converter`. #[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(); + native_output(&inputs); } -/// 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. -/// 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 — and it has: the 4f658c2b rev bump -/// dropped `openvm-kzg` from the sentence, so the expected text moved with it. -/// -/// Worth knowing why this can regress: `ethrex-crypto`'s own default feature set -/// is `["std", "kzg-rs", "secp256k1", "aws-lc-rs", "blst"]`, so any future -/// dependency pulling it in with defaults on restores a backend and silently -/// removes the screen. +/// The dependency graph intentionally has no KZG backend. Keep this check so +/// a future feature change cannot silently make the guest and host precompile +/// surfaces diverge. #[test] fn no_kzg_backend_linked() { - use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; + use ethrex_guest_program::crypto::Crypto; + 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(), @@ -144,30 +80,12 @@ fn no_kzg_backend_linked() { }; assert!( message.contains("One of features c-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}" + "a KZG backend is linked into ethrex-tests: {message}" ); } -/// 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"] +#[ignore = "real block through the VM; unmeasured runtime"] #[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); + run_fixture(REAL_BLOCK_FIXTURE); } From cab67d33092f0a2acacb9a216a583dd495ea270c Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Tue, 8 Sep 2026 18:13:19 -0300 Subject: [PATCH 08/46] Re-pin the benchmark documentation and thresholds to the rebuilt workload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every figure carrying seconds or bytes in the bench workflows described the retired pre-Amsterdam block: 158.8 s per prove, 50.78M cycles, ~52 GB of peak heap, ~5.3 min per ABBA pair. The workload is now 20,360,647 cycles and 69.10 s at epoch 2^22 — mean of 10 proves on the bench runner, sd 0.41 s, CV 0.59%, 35.75 GiB peak, 466 MB bundle — so the durations the bot quotes, the ABBA pair table, the epoch trade-off and the run-count dial are re-measured rather than inherited, and the pre-bump figures are labelled as having measured a different fixture. The three deterministic counters are stated as such, since they are what tells a reader the workload is the one the job thinks it is. Two defects the re-read surfaced. The fixture's file rule depended on a phony target, so it regenerated on every invocation instead of only when missing. And the epoch and cost tables the converter's README used to carry went away when it was rewritten for SSZ, leaving seven references pointing at nothing; they now live in tooling/ethrex-fixtures/README.md, next to the generator that produces the workload. pr_main's justification for the detached ethrex-tests workspace also still cited rkyv's `unaligned` feature, which the bump removed. --- .github/workflows/bench-abba.yml | 34 +++++++------ .github/workflows/benchmark-gpu.yml | 9 ++-- .github/workflows/benchmark-pr.yml | 75 +++++++++++++++++------------ .github/workflows/pr_main.yaml | 8 +-- Makefile | 16 ++++-- scripts/bench_verify.sh | 2 +- scripts/perf_diff.sh | 2 +- tooling/ethrex-fixtures/README.md | 22 +++++++++ 8 files changed, 108 insertions(+), 60 deletions(-) diff --git a/.github/workflows/bench-abba.yml b/.github/workflows/bench-abba.yml index 23e323d57..afb8da0bc 100644 --- a/.github/workflows/bench-abba.yml +++ b/.github/workflows/bench-abba.yml @@ -10,18 +10,23 @@ name: Bench ABBA tiebreaker # # Resolution, from the paired t-test (resolvable 95% delta = t* x sd / sqrt(N)). The # pair-delta sd on this runner is NOT yet measured; the two columns bracket it between -# 1.0% (the GPU box's measured 0.64% plus margin) and 2.0% (sqrt(2) x this runner's -# measured 1.43% single-run CV): +# 0.6% and 0.85% (sqrt(2) x this runner's measured 0.59% single-run CV, from the +# 10-prove baseline in tooling/ethrex-fixtures/README.md): # -# pairs wall resolves (sd 2.0% / sd 1.0%) -# 8 ~50 min 1.7% / 0.8% -# 12 ~72 min 1.3% / 0.6% <- default -# 20 ~1h55m 0.9% / 0.5% -# 32 ~3h 0.7% / 0.4% +# pairs wall resolves (sd 0.85% / sd 0.6%) +# 8 ~26 min 0.71% / 0.50% +# 12 ~36 min 0.54% / 0.38% <- default +# 20 ~54 min 0.40% / 0.28% +# 32 ~1h22m 0.31% / 0.22% # -# Wall is 2 x 158.8 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min +# Wall is 2 x 69.10 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min # of setup. The FIRST run measures the sd — it is the `sd` field of the paired-t line # in the result comment — and this table should be re-pinned to it then. +# +# Both columns tightened when the workload was rebuilt for ethrex 25: the block is +# 20.36M cycles instead of 50.78M and the single-run CV fell from 1.43% to 0.59%, so a +# pair now costs 2.3 min instead of 5.3. Numbers from before that bump measured a +# different fixture and are not comparable. on: issue_comment: types: [created] @@ -29,7 +34,8 @@ on: env: # The epoch /bench proves this block at, so the tiebreaker resolves the same # configuration and not just the same block. Memory picks it: this runner peaks at - # ~52 GB here against its >=64 GiB floor. See benchmark-pr.yml's REAL_BLOCK_EPOCH_LOG2. + # 35.75 GiB here against its >=64 GiB floor, and 2^23 would take that to 51.46 GiB + # for 7.5% of wall. See benchmark-pr.yml's REAL_BLOCK_EPOCH_LOG2. ABBA_REAL_EPOCH_LOG2: "22" concurrency: @@ -50,8 +56,8 @@ jobs: startsWith(github.event.comment.body, '/bench-abba') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) runs-on: [self-hosted, bench] - # Hang guardrail, not expected duration: a real-block pair is 2 x 158.8 s = ~5.3 min, - # so the default 12 pairs runs ~72 min and the 40-pair clamp ~3.7 hr, plus up to + # Hang guardrail, not expected duration: a real-block pair is 2 x 69.10 s = ~2.3 min, + # so the default 12 pairs runs ~36 min and the 40-pair clamp ~1.6 hr, plus up to # ~30 min of two-sided build on a cold cache. timeout-minutes: 360 steps: @@ -117,9 +123,9 @@ jobs: await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - // A pair is TWO proves at the runner's measured 158.8 s, so ~5.3 min/pair, - // plus ~8 min of checkout, two-sided build and fixture fetch. - body: `⏳ **ABBA tiebreaker started** on the bench server: ${process.env.PAIRS} pairs of ${process.env.WORKLOAD} — a pair is 2 proves at ~158.8 s, so roughly ${Math.round(8 + Number(process.env.PAIRS) * 5.3)} min. Pass a smaller pair count for a quicker, coarser run. The bench server is occupied until it finishes.` + // A pair is TWO proves at the runner's measured 69.10 s, so ~2.3 min/pair, + // plus ~8 min of checkout, two-sided build and fixture build. + body: `⏳ **ABBA tiebreaker started** on the bench server: ${process.env.PAIRS} pairs of ${process.env.WORKLOAD} — a pair is 2 proves at ~69 s, so roughly ${Math.round(8 + Number(process.env.PAIRS) * 2.3)} min. Pass a smaller pair count for a quicker, coarser run. The bench server is occupied until it finishes.` }); - name: Checkout (full history for ref resolution) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 4a9c33398..ac4d09310 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -9,7 +9,7 @@ name: Benchmark GPU (PR) # Triggered by a "/bench-gpu [N]" comment on a PR (N = pair count, default 14) or via # workflow_dispatch. # -# Workload: the real block (see tooling/ethrex-block-converter/README.md), proven with +# Workload: the real block (see tooling/ethrex-fixtures/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). @@ -231,9 +231,12 @@ jobs: run: | # 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. + # The workload rebuilt for ethrex 25 peaks at 35.75 GiB on the CPU path + # (10-prove baseline of 2026-09-08), so the floor keeps its headroom; it is + # the epoch, not the block, that would move it. # 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). + # don't move it; raising the epoch would (see the epoch table in + # tooling/ethrex-fixtures/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 diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 934dd1996..4a1b01410 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -70,10 +70,15 @@ env: ELF: executor/program_artifacts/rust/ethrex.elf # 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 - # ~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. + # moves this job without editing it. At the current default that is 20,360,647 + # cycles, 2,701 keccak calls and 116 ecsm calls: 7.5x the work of the synthetic + # block (ethrex_bench_4, 2,727,681 cycles) with a keccak:ecrecover mix of 23:1 + # against its 13:1. That is the whole point — a prover change can move the + # synthetic number and the real one in opposite directions. + # + # Those three counts are deterministic for a given ELF and input, so they are the + # check that the workload is what this job thinks it is; every figure below that + # carries seconds or bytes is machine-dependent and was measured on THIS runner. # # The path is resolved from the Makefile (`make -s print-real-block-fixture`) into # REAL_INPUT at run time. The .bin is gitignored and BUILT from the block's replay @@ -83,30 +88,32 @@ 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 ~240 GB and a heavier candidate far more. + # current default would need ~100 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-block-converter/README.md. + # trace length. Costs move with the block; what this one costs is in + # tooling/ethrex-fixtures/README.md. + # + # Budget ~470 MB of disk for the bundle each run (measured: 466,015,152 bytes) — + # hence the `rm -f` after every prove. A heavier block pushes it past 2 GiB, which + # needs rkyv `pointer_width_64` in the PROVER's bundle serialisation; a PR branch + # predating that fix fails at write time rather than mismeasuring. # - # 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, re-measured on THIS runner on 2026-09-08 for the rebuilt Amsterdam + # workload (10 proves at 2^22, one each at 2^21 and 2^23; box idle): # - # 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-block-converter/README.md): + # 2^21 78.54 s 23.93 GiB RSS 683 MB proof + # 2^22 69.10 s 35.75 GiB RSS 466 MB proof <- default + # 2^23 63.93 s 51.46 GiB RSS 378 MB proof # - # 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 + # Memory, not speed, is what picks 2^22: 2^23 buys 7.5% of wall but leaves little + # margin against this runner's >=64 GiB floor, and 2^21 pays 13.7% of wall to save + # 12 GiB nobody needs. The 2^22 line is the baseline the A/B leans on: mean 69.10 s + # over 10 runs, sd 0.41 s, CV 0.59%, whole range 1.23 s. Verification of one bundle + # is 6.86 s. Same table, with the epoch trade-off, in tooling/ethrex-fixtures/README.md. # - # Those RSS figures are the CALIBRATION BOX's, and memory does not transfer between - # machines any better than seconds do: this runner measured ~52 GB of peak heap for - # the same block at 2^22 — over 1.5x the calibration box's 32.21 GiB, and close - # enough to its >=64 GiB floor that 2^23 (60 GiB on the roomier box, so more here) - # is out of the question. Memory, not speed, is what picks 2^22. Moving off 2^21 is - # worth ~14% wall on the calibration box; treat that ratio as transferable and - # neither the absolute seconds nor the RSS column as such. This runner's own - # measured time at 2^22 is 158.8 s (median of 3, 2.8% spread, 13 epochs). + # The pre-bump numbers this block replaces (50.78M cycles, 158.8 s at 2^22 on this + # runner, ~52 GB) measured a DIFFERENT workload: that fixture was an rkyv + # ProgramInput, which ethrex 25's guest rejects. Do not compare across the bump. # # 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 @@ -116,11 +123,15 @@ env: # 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. A run measures 158.8 s on this runner (median of 3, 2.8% spread, - # at the epoch above), so 3 runs is ~8 min of proving and the right count. Should a + # THIS IS THE DIAL. A run measures 69.10 s on this runner (mean of 10, CV 0.59%, at + # the epoch above), so 3 runs is ~3.5 min of proving and the right count. Should a # future block or prover change take a run past ~6 min, /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. + # + # What 3 runs can actually resolve, from that sd: a two-sided 95% delta of ~2.08%. + # Five per side gets to ~1.04% and ten to ~0.60%, so a sub-2% claim needs `/bench N` + # (clamped to 5) or the ABBA tiebreaker, not a re-read of a 3-run table. # `/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, and it # proves this same block at this same epoch, so escalating keeps the question fixed. @@ -225,10 +236,10 @@ jobs: # not need a second command to ask for, and it is the only workload /bench # proves. # - # The cost is real and lands on a shared runner: 158.8 s per run x - # BENCH_RUNS_REAL is ~8 min of proving, so every /bench and every push to main - # occupies the bench server for roughly 15 min once checkout, the two-sided - # build, the fixture fetch and the guest ELF are counted, and /bench-abba and + # The cost is real and lands on a shared runner: 69.10 s per run x + # BENCH_RUNS_REAL is ~3.5 min of proving, so every /bench and every push to main + # occupies the bench server for roughly 10 min once checkout, the two-sided + # build, the fixture build and the guest ELF are counted, 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 @@ -269,9 +280,9 @@ jobs: # Clamp to 1-5. Beyond 5 the single-session cached comparison barely improves # (it can't beat the ~1% drift wall); use /bench-abba for finer deltas — it - # resolves ~1% over the same block in ~72 min at its default 12 pairs. - # At 158.8 s a run this is also the difference between a ~10 min and a ~20 min - # occupancy of the one bench runner. + # resolves ~0.5% over the same block in ~36 min at its default 12 pairs. + # At 69.10 s a run this is also the difference between a ~7 min and a ~12 min + # occupancy of the one bench runner. Three runs resolve ~2.08%, five ~1.04%. 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_REAL" RUNS=$BENCH_RUNS_REAL diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index 767e166de..08e64c086 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -131,10 +131,10 @@ jobs: cargo test --release -p executor test_ckzg -- --ignored # 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. + # workspace (the ethrex host dependency tree is heavy and pinned by rev in its + # own Cargo.lock, kept out of the main build), so run them from that directory + # to use that lockfile. The guest ELF and committed fixtures are already + # present from the steps above. # --include-ignored also runs the heavier synthetic-block test. # # `--skip test_ethrex_real_block` is a substring match, so it drops BOTH diff --git a/Makefile b/Makefile index ced7743f2..2c1400322 100644 --- a/Makefile +++ b/Makefile @@ -303,8 +303,8 @@ 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-block-converter/README.md carries the procedure and each candidate's -# measured cost. +# tooling/ethrex-fixtures/README.md carries what the workload costs and how to pick +# the epoch size; the converter's README covers converting a cache by hand. ETHREX_REAL_BLOCK_NETWORK := mainnet ETHREX_REAL_BLOCK := 25368371 # The fixture is GENERATED from the cache below, not fetched. ethrex 25's guest @@ -362,8 +362,9 @@ define ensure_verified 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-block-converter/README.md for how to produce and host one." >&2; \ + echo " Makefile to wherever it is hosted. The FIXTURE is not fetched at all -" >&2; \ + echo " it is built by 'make regen-real-block-fixture'; see" >&2; \ + echo " tooling/ethrex-fixtures/README.md." >&2; \ exit 1; \ fi; \ mkdir -p $(dir $(3)); \ @@ -391,7 +392,12 @@ endef # cargo build to the critical path. ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) -$(ETHREX_REAL_BLOCK_FIXTURE): ethrex-real-block-cache +# No prerequisites on purpose. `ethrex-real-block-cache` is phony (so its digest is +# re-checked on every call), and a phony prerequisite always reads as newer than its +# target, which would rebuild the fixture on every single benchmark invocation. The +# cache is fetched from inside `regen-real-block-fixture` instead, so this recipe runs +# only when the file is genuinely missing. +$(ETHREX_REAL_BLOCK_FIXTURE): $(MAKE) regen-real-block-fixture ethrex-real-block-cache: diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 369041c48..9deaea0df 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -37,7 +37,7 @@ # CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). 20 is the # laptop-safe setting, not the fast one: 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-block-converter/README.md, "Choosing the epoch size"), +# one (see tooling/ethrex-fixtures/README.md, "Choosing the epoch size"), # which is what /bench and /bench-abba pin. (`cli prove --epoch-size-log2 --help` # measured ethrex 10tx at ~9.5 GB for 2^20 vs ~15.8 GB for 2^21.) Note this does # NOT match bench_recursion_cycles.sh's BLOCK_EPOCH_LOG2=21: that arm needs FEW diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 2c359a548..e6e949fb7 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -16,7 +16,7 @@ # Env: WORKLOAD=real|synthetic (default real) 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-block-converter/README.md, "Choosing the epoch size"). +# 128 GiB box (tooling/ethrex-fixtures/README.md, "Choosing the epoch size"). # # Pick the workload that matches the run you are localizing, because the symbol # mix follows the block: the real default is 50.78M cycles, 10,478 keccak calls and diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 04b734860..f7a5f062c 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -82,3 +82,25 @@ repository root has both sets measured side by side. Measured on the guest ELF at ethrex `2cb18b0b`: 20,360,647 cycles, 2,701 keccak calls, 116 ECSM calls. + +### Choosing the epoch size, and what the workload costs + +Measured on the bench runner (`vm-benchmarks-1`, 96 cores / 125 GB, idle) on +2026-09-08, with the guest ELF at ethrex `2cb18b0b`: + +| epoch | wall | host RSS | proof | +| ---: | ---: | ---: | ---: | +| 2^21 | 78.54 s | 23.93 GiB | 683 MB | +| **2^22** | **69.10 s** (mean of 10, sd 0.41 s, CV 0.59 %) | **35.75 GiB** | **466 MB** | +| 2^23 | 63.93 s | 51.46 GiB | 378 MB | + +Verifying one 2^22 bundle takes 6.86 s. 2^22 is what `/bench`, `/bench-abba` and +the GPU bench pin: 2^23 buys 7.5 % of wall but leaves little margin against the +runner's 64 GiB floor, and 2^21 pays 13.7 % to save memory nobody needs. + +With that sd, a two-sided 95 % comparison resolves ~2.08 % at three runs per +side, ~1.04 % at five and ~0.60 % at ten — which is why a sub-2 % claim needs +`/bench N` or the ABBA tiebreaker rather than a re-read of a three-run table. + +Continuations are not optional here: monolithic proving costs ~4.9 GB of peak +heap per million cycles on this family, so 20.36M cycles would need ~100 GB. From 349d2033b296cbf7e22ab8153782af7aedeca7d8 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Tue, 8 Sep 2026 19:28:03 -0300 Subject: [PATCH 09/46] Repoint the benchmark to mainnet 25453112, the closest Amsterdam-rebuilt equivalent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under Amsterdam, 25368371 loses so much of its gas to the new state-gas model that the workload it rebuilds into is 20.36M cycles against the 30.50M the retired fixture costs on today's guest — a third lighter than what /bench used to prove. Screening twelve real mainnet blocks (both release caches plus ethrex's curated zkevm_bench corpus) through the generator put 25453112 closest at 37.14M cycles, +22% against that target, where the next candidate up lands at +197%. Its cache is already hosted in bench-fixtures-v1 with a published digest, so the repoint publishes nothing: block number, cache URL and sha256 in the Makefile, and the fixture name the usability screen reads. Reverts are not a selection criterion. Every pre-Amsterdam block loses 26-50% of its transactions to the fork, which is a property of the fork rather than of any block, and several of the twelve end up consuming more gas than they did on mainnet because Amsterdam makes the surviving transactions dearer. Measured on the bench runner at epoch 2^22, five proves, box idle: 125.17 s mean, sd 0.71 s, CV 0.57%, 43.00 GiB peak, 790 MB bundle, 12.0 s to verify. Every seconds-or-bytes figure in the bench workflows moves with it, as does what three runs can resolve (~2.00%). One behavioural change comes with it: the GPU arm's cpu_ram floor goes from 48 GB to 64. At 43.00 GiB the peak is 96% of a 48 GB box, which is an OOM waiting for a rental with a hungrier allocator; 64 GB restores the headroom the old floor had and matches the CPU runner's. It costs offer availability. --- .github/workflows/bench-abba.yml | 36 ++++++------- .github/workflows/benchmark-gpu.yml | 19 ++++--- .github/workflows/benchmark-pr.yml | 58 ++++++++++---------- Makefile | 15 ++++-- tooling/ethrex-fixtures/README.md | 80 +++++++++++++++------------- tooling/ethrex-tests/tests/ethrex.rs | 2 +- 6 files changed, 114 insertions(+), 96 deletions(-) diff --git a/.github/workflows/bench-abba.yml b/.github/workflows/bench-abba.yml index afb8da0bc..c691fd5b2 100644 --- a/.github/workflows/bench-abba.yml +++ b/.github/workflows/bench-abba.yml @@ -10,23 +10,23 @@ name: Bench ABBA tiebreaker # # Resolution, from the paired t-test (resolvable 95% delta = t* x sd / sqrt(N)). The # pair-delta sd on this runner is NOT yet measured; the two columns bracket it between -# 0.6% and 0.85% (sqrt(2) x this runner's measured 0.59% single-run CV, from the -# 10-prove baseline in tooling/ethrex-fixtures/README.md): +# 0.6% and 0.81% (sqrt(2) x this runner's measured 0.57% single-run CV, from the +# 5-prove baseline in tooling/ethrex-fixtures/README.md): # -# pairs wall resolves (sd 0.85% / sd 0.6%) -# 8 ~26 min 0.71% / 0.50% -# 12 ~36 min 0.54% / 0.38% <- default -# 20 ~54 min 0.40% / 0.28% -# 32 ~1h22m 0.31% / 0.22% +# pairs wall resolves (sd 0.81% / sd 0.6%) +# 8 ~41 min 0.67% / 0.50% +# 12 ~58 min 0.51% / 0.38% <- default +# 20 ~1h31m 0.38% / 0.28% +# 32 ~2h22m 0.29% / 0.22% # -# Wall is 2 x 69.10 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min +# Wall is 2 x 125.17 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min # of setup. The FIRST run measures the sd — it is the `sd` field of the paired-t line # in the result comment — and this table should be re-pinned to it then. # -# Both columns tightened when the workload was rebuilt for ethrex 25: the block is -# 20.36M cycles instead of 50.78M and the single-run CV fell from 1.43% to 0.59%, so a -# pair now costs 2.3 min instead of 5.3. Numbers from before that bump measured a -# different fixture and are not comparable. +# Both columns tightened when the workload was rebuilt for ethrex 25: the single-run +# CV fell from 1.43% to 0.57%, so 12 pairs now resolve ~0.5% where they used to resolve +# ~1.3%. Numbers from before that bump measured a different fixture (block 25368371 as +# an rkyv ProgramInput) and are not comparable. on: issue_comment: types: [created] @@ -34,8 +34,8 @@ on: env: # The epoch /bench proves this block at, so the tiebreaker resolves the same # configuration and not just the same block. Memory picks it: this runner peaks at - # 35.75 GiB here against its >=64 GiB floor, and 2^23 would take that to 51.46 GiB - # for 7.5% of wall. See benchmark-pr.yml's REAL_BLOCK_EPOCH_LOG2. + # 43.00 GiB here against its >=64 GiB floor, and 2^23 would take that past 50 GiB + # for ~7% of wall. See benchmark-pr.yml's REAL_BLOCK_EPOCH_LOG2. ABBA_REAL_EPOCH_LOG2: "22" concurrency: @@ -56,8 +56,8 @@ jobs: startsWith(github.event.comment.body, '/bench-abba') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) runs-on: [self-hosted, bench] - # Hang guardrail, not expected duration: a real-block pair is 2 x 69.10 s = ~2.3 min, - # so the default 12 pairs runs ~36 min and the 40-pair clamp ~1.6 hr, plus up to + # Hang guardrail, not expected duration: a real-block pair is 2 x 125.17 s = ~4.2 min, + # so the default 12 pairs runs ~58 min and the 40-pair clamp ~2.9 hr, plus up to # ~30 min of two-sided build on a cold cache. timeout-minutes: 360 steps: @@ -123,9 +123,9 @@ jobs: await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - // A pair is TWO proves at the runner's measured 69.10 s, so ~2.3 min/pair, + // A pair is TWO proves at the runner's measured 125.17 s, so ~4.2 min/pair, // plus ~8 min of checkout, two-sided build and fixture build. - body: `⏳ **ABBA tiebreaker started** on the bench server: ${process.env.PAIRS} pairs of ${process.env.WORKLOAD} — a pair is 2 proves at ~69 s, so roughly ${Math.round(8 + Number(process.env.PAIRS) * 2.3)} min. Pass a smaller pair count for a quicker, coarser run. The bench server is occupied until it finishes.` + body: `⏳ **ABBA tiebreaker started** on the bench server: ${process.env.PAIRS} pairs of ${process.env.WORKLOAD} — a pair is 2 proves at ~125 s, so roughly ${Math.round(8 + Number(process.env.PAIRS) * 4.2)} min. Pass a smaller pair count for a quicker, coarser run. The bench server is occupied until it finishes.` }); - name: Checkout (full history for ref resolution) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index ac4d09310..2fec522bb 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -229,11 +229,16 @@ 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: the real block at epoch 2^22 peaks at - # ~36 GB host RSS on the CUDA path (measured, main vintage) — ~25% headroom. - # The workload rebuilt for ethrex 25 peaks at 35.75 GiB on the CPU path - # (10-prove baseline of 2026-09-08), so the floor keeps its headroom; it is - # the epoch, not the block, that would move it. + # cpu_ram filter is in GB. Floor 64 GB, raised from 48 with the ethrex 25 + # workload: that block peaks at 43.00 GiB (46.2 GB) at epoch 2^22 on the CPU + # path, which on a 48 GB box is 96% of RAM and an OOM waiting for a rental + # with a slightly hungrier allocator. 64 GB restores the ~28% headroom the + # 48 GB floor had for the old block (~36 GB host RSS on the CUDA path, + # measured at main vintage) and matches the CPU runner's own floor. It costs + # offer availability: fewer boxes qualify, so PRICE_CAP may need a look if + # the query starts coming back empty. Peak is set by the epoch size, not the + # block, so dropping GPU_REAL_EPOCH_LOG2 to 21 is the other lever (~24 GiB, + # +14% wall). # Continuation peak is set by the epoch size, not the block, so bigger blocks # don't move it; raising the epoch would (see the epoch table in # tooling/ethrex-fixtures/README.md). @@ -249,7 +254,7 @@ jobs: # at the toolchain sanity gate. `reliability` is the queryable field (the # `reliability2` in the response schema is display-only, not filterable). # Over-strict just yields no offers, surfaced by the retry loop's "No offer". - QUERY="gpu_name=${GPU_NAME} num_gpus=1 gpu_frac=1 cpu_cores_effective>=16 cpu_cores_effective<=32 cpu_ram>=48 disk_space>=64 verified=true rentable=true reliability>=0.95 cuda_max_good>=12.8 dph_total<=${PRICE_CAP}" + QUERY="gpu_name=${GPU_NAME} num_gpus=1 gpu_frac=1 cpu_cores_effective>=16 cpu_cores_effective<=32 cpu_ram>=64 disk_space>=64 verified=true rentable=true reliability>=0.95 cuda_max_good>=12.8 dph_total<=${PRICE_CAP}" echo "Query: $QUERY (+ client-side driver_version major >= $MIN_DRIVER)" # Keep only offers whose driver major >= MIN_DRIVER, then most expensive first # (within the price cap). Within the now whole-machine pool, price just tracks @@ -498,7 +503,7 @@ jobs: # That surfaces as the OOM killer reaping a rustc ("signal: 9") or as an # allocation failure mid-compile. (Distinct from the toolchain gate's concern # above, which is a host that is broken before any load is applied.) - # 8 leaves ~6 GB/job on the >=48 GB floor; the build is a one-time per-bench + # 8 leaves ~8 GB/job on the >=64 GB floor; the build is a one-time per-bench # cost, and the job timeout above has ample room for it. REMOTE="set -e; cd /workspace/lambda_vm; \ command -v python3 >/dev/null || { apt-get update -qq && apt-get install -y -qq python3; }; \ diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 4a1b01410..8eb2d3f9c 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -70,9 +70,9 @@ env: ELF: executor/program_artifacts/rust/ethrex.elf # 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 20,360,647 - # cycles, 2,701 keccak calls and 116 ecsm calls: 7.5x the work of the synthetic - # block (ethrex_bench_4, 2,727,681 cycles) with a keccak:ecrecover mix of 23:1 + # moves this job without editing it. At the current default that is 37,137,386 + # cycles, 6,003 keccak calls and 164 ecsm calls: 13.6x the work of the synthetic + # block (ethrex_bench_4, 2,727,681 cycles) with a keccak:ecrecover mix of 37:1 # against its 13:1. That is the whole point — a prover change can move the # synthetic number and the real one in opposite directions. # @@ -88,32 +88,32 @@ 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 ~100 GB and a heavier candidate far more. + # current default would need ~182 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; what this one costs is in # tooling/ethrex-fixtures/README.md. # - # Budget ~470 MB of disk for the bundle each run (measured: 466,015,152 bytes) — + # Budget ~790 MB of disk for the bundle each run (measured: 828,430,552 bytes) — # hence the `rm -f` after every prove. A heavier block pushes it past 2 GiB, which # needs rkyv `pointer_width_64` in the PROVER's bundle serialisation; a PR branch # predating that fix fails at write time rather than mismeasuring. # - # Epoch 2^22, re-measured on THIS runner on 2026-09-08 for the rebuilt Amsterdam - # workload (10 proves at 2^22, one each at 2^21 and 2^23; box idle): + # Epoch 2^22, measured on THIS runner on 2026-09-08 (box idle): mean 125.17 s over + # 5 proves, sd 0.71 s, CV 0.57%, 43.00 GiB peak RSS, 790 MB bundle; verifying one + # bundle is 12.0 s. # - # 2^21 78.54 s 23.93 GiB RSS 683 MB proof - # 2^22 69.10 s 35.75 GiB RSS 466 MB proof <- default - # 2^23 63.93 s 51.46 GiB RSS 378 MB proof - # - # Memory, not speed, is what picks 2^22: 2^23 buys 7.5% of wall but leaves little - # margin against this runner's >=64 GiB floor, and 2^21 pays 13.7% of wall to save - # 12 GiB nobody needs. The 2^22 line is the baseline the A/B leans on: mean 69.10 s - # over 10 runs, sd 0.41 s, CV 0.59%, whole range 1.23 s. Verification of one bundle - # is 6.86 s. Same table, with the epoch trade-off, in tooling/ethrex-fixtures/README.md. + # Memory, not speed, is what picks 2^22. The epoch trade-off was swept on the + # previous block (2^21 +13.7% wall / -12 GiB, 2^23 -7.5% wall / +16 GiB): peak RSS + # is set by the epoch size rather than the block, so the shape carries over even + # though the absolute seconds do not. 2^23 would take this workload past 50 GiB + # against the runner's >=64 GiB floor. Both tables in + # tooling/ethrex-fixtures/README.md. # - # The pre-bump numbers this block replaces (50.78M cycles, 158.8 s at 2^22 on this - # runner, ~52 GB) measured a DIFFERENT workload: that fixture was an rkyv - # ProgramInput, which ethrex 25's guest rejects. Do not compare across the bump. + # The pre-bump numbers this replaces (block 25368371, 50.78M cycles, 158.8 s at + # 2^22 on this runner, ~52 GB) measured a DIFFERENT workload: that fixture was an + # rkyv ProgramInput, which ethrex 25's guest rejects. Do not compare across the + # bump. This block was picked to land as close to it as an Amsterdam rebuild can — + # see the screen in the Makefile's ETHREX_REAL_BLOCK comment. # # 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 @@ -123,14 +123,14 @@ env: # 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. A run measures 69.10 s on this runner (mean of 10, CV 0.59%, at - # the epoch above), so 3 runs is ~3.5 min of proving and the right count. Should a + # THIS IS THE DIAL. A run measures 125.17 s on this runner (mean of 5, CV 0.57%, at + # the epoch above), so 3 runs is ~6.3 min of proving and the right count. Should a # future block or prover change take a run past ~6 min, /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. # - # What 3 runs can actually resolve, from that sd: a two-sided 95% delta of ~2.08%. - # Five per side gets to ~1.04% and ten to ~0.60%, so a sub-2% claim needs `/bench N` + # What 3 runs can actually resolve, from that sd: a two-sided 95% delta of ~2.00%. + # Five per side gets to ~1.00% and ten to ~0.58%, so a sub-2% claim needs `/bench N` # (clamped to 5) or the ABBA tiebreaker, not a re-read of a 3-run table. # `/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, and it @@ -236,9 +236,9 @@ jobs: # not need a second command to ask for, and it is the only workload /bench # proves. # - # The cost is real and lands on a shared runner: 69.10 s per run x - # BENCH_RUNS_REAL is ~3.5 min of proving, so every /bench and every push to main - # occupies the bench server for roughly 10 min once checkout, the two-sided + # The cost is real and lands on a shared runner: 125.17 s per run x + # BENCH_RUNS_REAL is ~6.3 min of proving, so every /bench and every push to main + # occupies the bench server for roughly 13 min once checkout, the two-sided # build, the fixture build and the guest ELF are counted, 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. @@ -280,9 +280,9 @@ jobs: # Clamp to 1-5. Beyond 5 the single-session cached comparison barely improves # (it can't beat the ~1% drift wall); use /bench-abba for finer deltas — it - # resolves ~0.5% over the same block in ~36 min at its default 12 pairs. - # At 69.10 s a run this is also the difference between a ~7 min and a ~12 min - # occupancy of the one bench runner. Three runs resolve ~2.08%, five ~1.04%. + # resolves ~0.5% over the same block in ~58 min at its default 12 pairs. + # At 125.17 s a run this is also the difference between a ~10 min and a ~20 min + # occupancy of the one bench runner. Three runs resolve ~2.00%, five ~1.00%. 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_REAL" RUNS=$BENCH_RUNS_REAL diff --git a/Makefile b/Makefile index 2c1400322..4c43f0ec7 100644 --- a/Makefile +++ b/Makefile @@ -306,7 +306,16 @@ test-rust: compile-programs-rust # tooling/ethrex-fixtures/README.md carries what the workload costs and how to pick # the epoch size; the converter's README covers converting a cache by hand. ETHREX_REAL_BLOCK_NETWORK := mainnet -ETHREX_REAL_BLOCK := 25368371 +ETHREX_REAL_BLOCK := 25453112 +# WHY THIS BLOCK: 25368371 was the pre-bump default, and under Amsterdam its +# transactions lose so much gas to the new state-gas model that the workload drops +# to 20.36M cycles against the 30.50M the retired fixture cost on today's guest. A +# screen of twelve real mainnet blocks (our two release caches plus ethrex's curated +# zkevm_bench corpus) put this one closest: 37.14M cycles, +22% against that target, +# where 25368371 is -33% and the next candidate up (25087308) is +197%. Reverts are +# not a selection criterion — every pre-Amsterdam block loses 26-50% of its +# transactions to the fork, which is a property of the fork and not of the block. +# # The fixture is GENERATED from the cache below, not fetched. ethrex 25's guest # decodes only the Amsterdam schema (`0x1501`) and mainnet has no Amsterdam fork, # so no hosted artifact for this block can be valid: the release's rkyv one now @@ -323,8 +332,8 @@ ETHREX_REAL_BLOCK_FIXTURE_SHA256 := # The block's source cache: an ethrex-replay dump, fork-independent, still the one # hosted in bench-fixtures-v1. Only the fixture rebuild reads it; converter 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 +ETHREX_REAL_BLOCK_CACHE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/cache_mainnet_25453112.json +ETHREX_REAL_BLOCK_CACHE_SHA256 := 20ffbbc1b051df9dfa6285f0ea7bd3f6d054db883aba92230ea20350f1b1d4ad ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK) ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index f7a5f062c..5ce78b66a 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -48,59 +48,63 @@ block touches arrives intact — registers a parent header at the block's real height, replays the transactions in the block's own order through the t8n entry point, and validates the result through the native guest before writing. -Output for mainnet 25368371 (29 transactions, 2,428,684 gas): +Output for mainnet 25453112 (38 transactions, 4,238,394 gas): ``` -installed 91 accounts / 117 storage slots / 91 codes -rebuilt #25368371 (29/29 txs, 1803938 gas, 12 reverted) +installed 132 accounts / 261 storage slots / 132 codes +rebuilt #25453112 (38/38 txs, 3761976 gas, 10 reverted) ``` -### Why 12 transactions revert, and why that is not a defect here +### Why 10 transactions revert, and why that is not a defect here Those transactions were signed with gas limits computed under Osaka. Amsterdam changes the gas model: cold account access goes from 2600 to 3000, and EIP-8037 carves a state-gas reservoir out of the transaction's excess gas limit. A transfer sent with a limit of exactly 21,000 has no excess to carve, so it runs -out of gas. The same rebuild under Osaka rules — `REAL_BLOCK_FORK=osaka`, which -prints the totals and exits without writing, since the guest only decodes the -Amsterdam schema — reverts **2** transactions instead of 12: - -| rules | txs | gas | reverted | -| --- | --- | ---: | ---: | -| Osaka (what the block was built for) | 29/29 | 1,473,522 | 2 | -| Amsterdam | 29/29 | 1,803,938 | 12 | -| mainnet, as mined | 29 | 2,428,684 | — | - -So the gap is the fork, not the reconstruction: a pre-Amsterdam block cannot be -a faithful Amsterdam workload at any level of tooling effort. What this fixture -is, therefore, is a *real-mix* Amsterdam block — real contract code, real -calldata, real signatures (the accelerator counters agree exactly with the -retired fixture at 116 ECSM calls), real trie depth — and not a replay of -mainnet economics. For workloads whose gas limits were computed for Amsterdam, -use the EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the -repository root has both sets measured side by side. - -Measured on the guest ELF at ethrex `2cb18b0b`: 20,360,647 cycles, 2,701 keccak -calls, 116 ECSM calls. +out of gas. Running the same rebuild under Osaka rules — `REAL_BLOCK_FORK=osaka`, +which prints the totals and exits without writing, since the guest only decodes +the Amsterdam schema — is what separates the fork from the reconstruction: on +block 25368371 it reverts 2 transactions where Amsterdam reverts 12. + +Every pre-Amsterdam block loses transactions this way. A screen of twelve real +mainnet blocks (both of our release caches plus ethrex's curated zkevm_bench +corpus) put the revert share between 26% and 50% with no exceptions, so it is not +a criterion for picking one. What the screen was for is weight: the retired +fixture cost 30.50M cycles on today's guest, and this block rebuilds to 37.14M +(+22%), the closest of the twelve — 25368371 comes in at -33% and the next +candidate up, 25087308, at +197%. Several blocks end up consuming *more* gas than +they did on mainnet (+7% to +22%), because Amsterdam makes the surviving +transactions dearer. + +So what this fixture is, is a *real-mix* Amsterdam block — real contract code, +real calldata, real signatures, real trie depth — and not a replay of mainnet +economics. For workloads whose gas limits were computed for Amsterdam, use the +EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the repository +root has both sets measured side by side. + +Measured on the guest ELF at ethrex `2cb18b0b`: 37,137,386 cycles, 6,003 keccak +calls, 164 ECSM calls. Fixture: 549,144 bytes. ### Choosing the epoch size, and what the workload costs -Measured on the bench runner (`vm-benchmarks-1`, 96 cores / 125 GB, idle) on -2026-09-08, with the guest ELF at ethrex `2cb18b0b`: +Measured on the bench runner (`vm-benchmarks-1`, which is also the CI +self-hosted `bench` runner: 96 cores / 125 GB, idle) on 2026-09-08, with the +guest ELF at ethrex `2cb18b0b`: -| epoch | wall | host RSS | proof | -| ---: | ---: | ---: | ---: | -| 2^21 | 78.54 s | 23.93 GiB | 683 MB | -| **2^22** | **69.10 s** (mean of 10, sd 0.41 s, CV 0.59 %) | **35.75 GiB** | **466 MB** | -| 2^23 | 63.93 s | 51.46 GiB | 378 MB | +| | mean | sd | CV | peak RSS | proof | verify | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| epoch 2^22, 5 proves | **125.17 s** | 0.71 s | 0.57 % | 43.00 GiB | 790 MB | 12.0 s | -Verifying one 2^22 bundle takes 6.86 s. 2^22 is what `/bench`, `/bench-abba` and -the GPU bench pin: 2^23 buys 7.5 % of wall but leaves little margin against the -runner's 64 GiB floor, and 2^21 pays 13.7 % to save memory nobody needs. +2^22 is what `/bench`, `/bench-abba` and the GPU bench pin. The epoch trade-off +itself was swept on the previous block (2^21 costs +13.7 % of wall to save +12 GiB; 2^23 buys −7.5 % for +16 GiB): peak RSS is set by the epoch size rather +than by the block, so that shape carries over even though the seconds do not. +2^23 would take this workload past 50 GiB against the runner's 64 GiB floor, +which is why memory and not speed picks the default. -With that sd, a two-sided 95 % comparison resolves ~2.08 % at three runs per -side, ~1.04 % at five and ~0.60 % at ten — which is why a sub-2 % claim needs +With that sd, a two-sided 95 % comparison resolves ~2.00 % at three runs per +side, ~1.00 % at five and ~0.58 % at ten — which is why a sub-2 % claim needs `/bench N` or the ABBA tiebreaker rather than a re-read of a three-run table. Continuations are not optional here: monolithic proving costs ~4.9 GB of peak -heap per million cycles on this family, so 20.36M cycles would need ~100 GB. +heap per million cycles on this family, so 37.14M cycles would need ~182 GB. diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 95943ad99..a28e9be77 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -56,7 +56,7 @@ fn test_ethrex_empty_block() { run_fixture("ethrex_empty_block.bin"); } -const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin"; +const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25453112.bin"; /// Host-only acceptance gate for the real SSZ stateless fixture produced by /// `tooling/ethrex-block-converter`. From f195b1362b00875d7089aff162c8113fbbb28c3e Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 12:37:35 -0300 Subject: [PATCH 10/46] Fire the real-block validation workflow on the fixture generator and cache its build. The usability screen now builds the fixture with tooling/ethrex-fixtures instead of downloading it, so a change to real_block.rs or genesis.json could land without the job that validates it ever running, and the ethrex host tree it compiles was outside the rust-cache workspaces list. --- .github/workflows/ethrex-block-converter.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index e8260e129..f8ca2a2ff 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -7,14 +7,15 @@ name: ethrex block-converter tests # 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). +# It fires on the things that can actually invalidate it: the fixture generator, the +# converter, the ethrex host-reference tests, and the Makefile (which holds the block +# pin, the cache URL and its sha256). on: workflow_dispatch: pull_request: branches: ["**"] paths: + - 'tooling/ethrex-fixtures/**' - 'tooling/ethrex-block-converter/**' - 'tooling/ethrex-tests/**' - 'Makefile' @@ -22,6 +23,7 @@ on: push: branches: ["main"] paths: + - 'tooling/ethrex-fixtures/**' - 'tooling/ethrex-block-converter/**' - 'tooling/ethrex-tests/**' - 'Makefile' @@ -81,6 +83,7 @@ jobs: shared-key: "lambda-vm-real-block-usable" cache-all-crates: "true" workspaces: | + tooling/ethrex-fixtures -> target tooling/ethrex-tests -> target # Built from the block's replay cache, not downloaded: ethrex 25's guest decodes From 7e562f6e48396e3d43f257ff649b7d8f39a65260 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 15:02:13 -0300 Subject: [PATCH 11/46] Drop the dead ETHREX_REAL_BLOCK_FIXTURE_URL and point the benchmark comment's diagnostic at ETHREX_REAL_BLOCK_CACHE_URL, which is the variable that actually gates the real-block step now that the fixture is generated instead of fetched: anyone following the old message would have gone looking at a variable that is permanently empty and read by nothing. Limit the minimum-cycle floor in bench_abba.sh to WORKLOAD=real. The synthetic fixtures are already gated twice, by their committed checksums and by the successful_validation assert in the host tests, while a small TX_COUNT lands near the floor legitimately -- a one-transfer block ran 1.80M cycles before the bump and an empty one 0.99M -- so applying it there would reject valid runs. Correct three comments the rebuild left stale: the EEST probe is committed rather than untracked, the real-block fixture is produced by tooling/ethrex-fixtures rather than by the converter, and the real block costs 37M cycles rather than 20M. Record in the Makefile that the fixture name is pinned in the host tests as well, so repointing the block needs that edit too; a glob there would silently pick up a stale fixture from an earlier block and pass. --- .github/workflows/benchmark-pr.yml | 2 +- Makefile | 5 ++++- scripts/bench_abba.sh | 10 ++++++++-- tooling/ethrex-tests/tests/eest_amsterdam_probe.rs | 6 ++++-- tooling/ethrex-tests/tests/ethrex.rs | 4 +++- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 8eb2d3f9c..78e359c91 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -978,7 +978,7 @@ jobs: if (!realTime) { 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`; + body += `Check that \`ETHREX_REAL_BLOCK_CACHE_URL\` is set in the Makefile and that the replay cache is reachable; the fixture is generated from it, and 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/Makefile b/Makefile index 4c43f0ec7..5068065ad 100644 --- a/Makefile +++ b/Makefile @@ -327,7 +327,6 @@ ETHREX_REAL_BLOCK := 25453112 # there. Read the caveat in tooling/ethrex-fixtures/README.md before quoting # numbers: Amsterdam's gas model (EIP-8037 state gas, cold access 2600 -> 3000) # makes 10 of this block's Osaka-era transactions run out of gas. -ETHREX_REAL_BLOCK_FIXTURE_URL := ETHREX_REAL_BLOCK_FIXTURE_SHA256 := # The block's source cache: an ethrex-replay dump, fork-independent, still the one # hosted in bench-fixtures-v1. Only the fixture rebuild reads it; converter TESTS @@ -416,6 +415,10 @@ ethrex-real-block-cache: # scripts/bench_abba.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. +# One consumer is NOT wired through here: `REAL_BLOCK_FIXTURE` in +# tooling/ethrex-tests/tests/ethrex.rs is a const, because a glob would happily +# pick up a stale fixture from an earlier block and pass. Repointing the block +# means editing it too; it fails on the missing file, loudly. # `-s` on the caller's side keeps the output clean. print-real-block-fixture: @echo $(ETHREX_REAL_BLOCK_FIXTURE) diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 8c1fe7d2d..c8fd107a8 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -169,14 +169,20 @@ INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" # improvement, in green, on both sides of the A/B. One cheap execution up front turns # that class of mistake -- stale fixture, wrong fork, fixture built against another # ethrex rev -- into a hard stop. The floor is far below any real block (the current -# one is ~20M cycles) and far above a rejected one. +# one is ~37M cycles) and far above a rejected one. +# +# Real workload only. The synthetic fixtures are regenerated by the same tool at the +# same rev and are gated twice already (`make check-ethrex-fixture-checksums` plus the +# `successful_validation == 1` assert in tooling/ethrex-tests), while a small TX_COUNT +# legitimately lands near this floor -- a 1-transfer block ran 1.80M cycles before the +# bump, an empty one 0.99M. Applying the floor there would reject valid runs. MIN_PLAUSIBLE_CYCLES=1000000 if [ ! -x ./target/release/cli ]; then cargo build --release -p cli >/dev/null fi workload_cycles="$(./target/release/cli execute "$ELF" --private-input "$INPUT" --cycles \ | awk '/^Cycles:/ {print $2}')" -if [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then +if [ "$WORKLOAD" = "real" ] && [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then echo "ERROR: the workload executed only ${workload_cycles:-0} cycles, below the" >&2 echo " ${MIN_PLAUSIBLE_CYCLES} floor. The guest almost certainly rejected the input:" >&2 echo " ELF $ELF" >&2 diff --git a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs index 9bf6743f0..3bd4f593f 100644 --- a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs +++ b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs @@ -1,5 +1,7 @@ -//! Throwaway probe (untracked): do the upstream EEST Amsterdam benchmark -//! fixtures execute on the guest this branch pins? +//! Probe: do the upstream EEST Amsterdam benchmark fixtures execute on the +//! guest this rev pins? Committed as the measuring tool for adding an EEST +//! workload alongside the real-mix one; its inputs are not committed, so on a +//! plain checkout it skips (see below) and costs CI nothing. //! //! Inputs are `statelessInputBytes` lifted verbatim out of //! `tests-zkevm-benchmark@v0.8.2` blockchain tests, so nothing is rebuilt host diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index a28e9be77..97963b560 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -59,7 +59,9 @@ fn test_ethrex_empty_block() { const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25453112.bin"; /// Host-only acceptance gate for the real SSZ stateless fixture produced by -/// `tooling/ethrex-block-converter`. +/// `tooling/ethrex-fixtures --bin real_block`. The fixture name is pinned here +/// as well as in the Makefile, so repointing the block needs both edits; a +/// missed one fails loudly on the missing file rather than proving less. #[test] fn test_ethrex_real_block_native() { let inputs = std::fs::read(format!("{FIXTURES_DIR}/{REAL_BLOCK_FIXTURE}")).unwrap(); From 9ba052c6c03806edb1efa04ef5a5a9a103815e48 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 15:04:16 -0300 Subject: [PATCH 12/46] Make the real-block generator refuse to write a fixture that carries less work than the block it claims to reproduce. Two paths could shrink it silently, and neither was caught by any existing guard, because both leave a block that is perfectly valid: the guest still commits successful_validation = 1, so the native-validation check passes and the benchmark reports the missing work as an improvement. The first is a dropped transaction. build_payload_t8n returns the ones it could not apply and the generator only printed them; the transaction mix is the entire reason this fixture exists over a synthetic block, so applying fewer than all of them is now an error. Screening candidate blocks does need the partial ones reported rather than refused -- the twelve-block screen saw shares from 2/3 to 557/676 -- so REAL_BLOCK_ALLOW_DROPS=1 keeps that usable. The second is a witness leaf that does not yield a 32-byte trie key. collect_leaves skipped those without a counter, which loses an account or a storage slot; the transaction reading it then sees zero and reverts, and the only symptom is a revert count that is already non-zero and expected. It now returns how many it could not use and the caller aborts. The printed installed-versus-collected counts could never have caught this: both are derived from collect_leaves' output, so they agree by construction. Also record in the README that reverting and being dropped are different things, since the revert discussion there reads as if a lost transaction were normal. --- tooling/ethrex-fixtures/README.md | 12 ++++- tooling/ethrex-fixtures/src/bin/real_block.rs | 50 ++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 5ce78b66a..64ac08064 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -69,7 +69,17 @@ block 25368371 it reverts 2 transactions where Amsterdam reverts 12. Every pre-Amsterdam block loses transactions this way. A screen of twelve real mainnet blocks (both of our release caches plus ethrex's curated zkevm_bench corpus) put the revert share between 26% and 50% with no exceptions, so it is not -a criterion for picking one. What the screen was for is weight: the retired +a criterion for picking one. + +Reverting is separate from being *dropped*: a reverted transaction was applied and +paid for its gas, while a dropped one never entered the block. This block applies +all 38 of its transactions, and the generator refuses to write a fixture that +applies fewer, because every other guard would still pass — a block with fewer +transactions is a valid block, so the loss would show up only as a smaller +benchmark. Screening candidate blocks does need the partial ones, so pass +`REAL_BLOCK_ALLOW_DROPS=1` for that; note also that the generator is not universal +(block 25087563 fails with `StateRootMismatch`), which is why screening is a +required step before pinning a different block. What the screen was for is weight: the retired fixture cost 30.50M cycles on today's guest, and this block rebuilds to 37.14M (+22%), the closest of the twelve — 25368371 comes in at -33% and the next candidate up, 25087308, at +197%. Several blocks end up consuming *more* gas than diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index b77d96d8a..175b260b1 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -196,23 +196,39 @@ fn build_stateless_input( /// Walk an embedded witness trie and return every `(32-byte key, value)` leaf it /// still holds. Pruned subtrees appear as `NodeRef::Hash` and are skipped: they /// are the proof siblings, which no transaction in the block reads. -fn collect_leaves(node: &Node, path: Nibbles, out: &mut Vec<(H256, Vec)>) { - let descend = |child: &NodeRef, path: Nibbles, out: &mut Vec<(H256, Vec)>| { +/// +/// Returns the number of leaves that could NOT be turned into a 32-byte key. In +/// a hashed trie there is no such thing, so a non-zero count means a malformed +/// witness node — and the caller must refuse to build on it. Dropping one +/// silently loses an account or a storage slot, the transaction that reads it +/// then sees zero and reverts, and the fixture ends up carrying less work than +/// the block it claims to reproduce with the revert count as its only symptom. +/// The installed-vs-collected counts cannot catch it: both are derived from this +/// function's output, so they agree by construction. +fn collect_leaves(node: &Node, path: Nibbles, out: &mut Vec<(H256, Vec)>) -> usize { + let descend = |child: &NodeRef, path: Nibbles, out: &mut Vec<(H256, Vec)>| -> usize { if let NodeRef::Node(child, _) = child { - collect_leaves(child, path, out); + collect_leaves(child, path, out) + } else { + 0 } }; match node { Node::Branch(branch) => { + let mut unusable = 0; for (i, child) in branch.choices.iter().enumerate() { - descend(child, path.append_new(i as u8), out); + unusable += descend(child, path.append_new(i as u8), out); } + unusable } Node::Extension(ext) => descend(&ext.child, path.concat(&ext.prefix), out), Node::Leaf(leaf) => { let bytes = path.concat(&leaf.partial).to_bytes(); if bytes.len() == 32 { out.push((H256::from_slice(&bytes), leaf.value.clone())); + 0 + } else { + 1 } } } @@ -283,8 +299,9 @@ async fn main() -> Result<(), Box> { genesis.timestamp = real_block.header.timestamp.saturating_sub(12); let mut accounts = Vec::new(); + let mut unusable_leaves = 0usize; if let Some(root) = &witness.state_trie_root { - collect_leaves(root, Nibbles::default(), &mut accounts); + unusable_leaves += collect_leaves(root, Nibbles::default(), &mut accounts); } let mut store = Store::new(".ethrex-real-block-tmp", EngineType::InMemory)?; @@ -312,7 +329,7 @@ async fn main() -> Result<(), Box> { .map_err(|e| format!("decode account {hashed_address:#x}: {e:?}"))?; if let Some(storage_root) = witness.storage_trie_roots.get(hashed_address) { let mut leaves = Vec::new(); - collect_leaves(storage_root, Nibbles::default(), &mut leaves); + unusable_leaves += collect_leaves(storage_root, Nibbles::default(), &mut leaves); let mut storage_trie = store.open_direct_storage_trie( *hashed_address, *ethrex_common::constants::EMPTY_TRIE_HASH, @@ -336,6 +353,14 @@ async fn main() -> Result<(), Box> { installed_accounts += 1; } let installed_root = state_trie.hash(&NativeCrypto)?; + if unusable_leaves > 0 { + return Err(format!( + "{unusable_leaves} witness leaf/leaves did not yield a 32-byte trie key, so \ + that much of the block's pre-state was never installed. The transactions \ + reading it would see zero and revert, shrinking the workload silently." + ) + .into()); + } // Build at the real height, not at #1. The seeded accounts hold the block's // own pre-state, but a contract that stores a block number and compares it @@ -442,6 +467,19 @@ async fn main() -> Result<(), Box> { let block = result.payload; let included = block.body.transactions.len(); + // The tx mix is the whole reason this fixture exists, so a builder that applied + // fewer than all of them has produced a smaller workload than the block it claims + // to reproduce -- and every guard below still passes, because a block with fewer + // transactions is a perfectly valid block. The screen sets the escape: surveying + // candidate blocks needs the partial ones reported, not refused. + if included != total_txs && std::env::var_os("REAL_BLOCK_ALLOW_DROPS").is_none() { + return Err(format!( + "the payload builder applied only {included} of {total_txs} transactions; \ + rejected: {rejected:?}. Set REAL_BLOCK_ALLOW_DROPS=1 to write the fixture \ + anyway." + ) + .into()); + } // --- 4. witness -> SSZ -> native validation ---------------------------- let witness = blockchain From f16e360e2b8d5946c1b32b1dfdfcc00f34925100 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 15:24:36 -0300 Subject: [PATCH 13/46] Drop the unused Address and U256 imports from the real-block generator. They arrived with the file and no CI job would have reported them: `make lint` only covers the root workspace, and tooling/ethrex-fixtures is a detached one, so the ethrex host tree it pulls stays out of the main build. Checked with cargo check --all-targets in the three detached crates the branch touches; this was the only warning in any of them. --- tooling/ethrex-fixtures/src/bin/real_block.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index 175b260b1..ab6e637f8 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -34,7 +34,7 @@ use ethrex_common::types::stateless_ssz::{ STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, }; use ethrex_common::types::{AccountState, Block, ELASTICITY_MULTIPLIER, Genesis}; -use ethrex_common::{Address, H256, U256}; +use ethrex_common::H256; use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; use ethrex_guest_program::l1::run_stateless_guest; use ethrex_rlp::decode::RLPDecode; From b0641ac83b88b52dd7eb6a83f6831e652d362d5a Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 15:29:23 -0300 Subject: [PATCH 14/46] Pin the generated real-block fixture by sha256 and verify it where the fixture is used, not only where it is produced. The variable was already there and empty; filling it closes the one path by which a benchmark could measure the wrong workload without anything going red. Generating the fixture validates the block through the guest, but generation only runs when the file is MISSING -- deliberately, so that a benchmark does not pay for a cargo build -- and the bench runner is persistent. A fixture built before a rev bump therefore survives it, and every benchmark keeps proving the old workload. The minimum-cycle floor in bench_abba.sh cannot see it either: a stale fixture still runs tens of millions of cycles, and both sides of an A/B read the same file, so the delta looks healthy while the absolute numbers belong to a different block. The Makefile comment claimed a stale fixture could not reach a benchmark; that claim is now true rather than aspirational. Hashing 549 KB is free and only a mismatch pays for a rebuild, so the original reason for not re-checking on every invocation is preserved. On mismatch the fixture is regenerated and re-checked, which self-heals the stale case; if the fresh output still differs, the build stops and says that the inputs or the output format moved and the recorded numbers no longer describe the workload. An empty digest disables the check and announces it, which is the state right after repointing the block and before the new digest is known. Safe to pin because the generator is deterministic across machines, not just across runs: the same 549,144 bytes and the same 08a52e10 digest come out on macOS arm64 and on the Linux x86-64 bench runner, from the same installed state root 0xce4c19cd. --- Makefile | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 5068065ad..52205dd40 100644 --- a/Makefile +++ b/Makefile @@ -322,12 +322,24 @@ ETHREX_REAL_BLOCK := 25453112 # runs 496 cycles and commits `successful_validation = 0` instead of failing. # `tooling/ethrex-fixtures --bin real_block` rebuilds the block's real # transactions and real pre-state as an Amsterdam block and validates the result -# through the guest before writing it, so a stale or wrong-fork fixture cannot -# reach a benchmark. Nothing to publish, and it works offline once the cache is -# there. Read the caveat in tooling/ethrex-fixtures/README.md before quoting -# numbers: Amsterdam's gas model (EIP-8037 state gas, cold access 2600 -> 3000) -# makes 10 of this block's Osaka-era transactions run out of gas. -ETHREX_REAL_BLOCK_FIXTURE_SHA256 := +# through the guest before writing it. Nothing to publish, and it works offline +# once the cache is there. Read the caveat in tooling/ethrex-fixtures/README.md +# before quoting numbers: Amsterdam's gas model (EIP-8037 state gas, cold access +# 2600 -> 3000) makes 10 of this block's Osaka-era transactions run out of gas. +# +# The digest below is what keeps a stale fixture out of a benchmark. Generating +# validates the block, but generation only happens when the file is MISSING (see +# the rule), and the bench runner is persistent -- so without this, a fixture +# built before a rev bump survives it and every benchmark keeps proving the old +# workload. The minimum-cycle floor in bench_abba.sh cannot see that either: a +# stale fixture still runs tens of millions of cycles, and both sides of an A/B +# read the same file, so the delta looks healthy while the absolute numbers +# belong to the wrong block. Verified byte-identical on macOS arm64 and on the +# Linux x86-64 runner, which is what makes pinning it safe. Repointing the block +# or moving the ethrex rev changes it: run `make regen-real-block-fixture`, take +# the new digest, and paste it here. Leaving it empty disables the check and says +# so out loud. +ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 08a52e10c2f89870dade14f7b0dfe01625e57416afbff664ad31fcd067233eea # The block's source cache: an ethrex-replay dump, fork-independent, still the one # hosted in bench-fixtures-v1. Only the fixture rebuild reads it; converter TESTS # use a different, upstream-pinned cache (below). @@ -395,10 +407,33 @@ endef # A file rule, unlike the fetched artifacts above: those are phony so their digest # is re-checked on every invocation, which is how a stale download is caught. This # one is BUILT from the cache (whose digest IS re-checked) by a deterministic -# generator that validates the block through the guest before writing, so the file -# existing is proof enough and rebuilding it on every benchmark would just add a -# cargo build to the critical path. +# generator, so its digest is re-checked here the same way -- hashing 549 KB costs +# nothing, and only a MISMATCH pays for a rebuild, which is what keeps a cargo +# build off every benchmark's critical path. ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) + @set -e; \ + want="$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)"; \ + if [ -z "$$want" ]; then \ + echo "::warning::ETHREX_REAL_BLOCK_FIXTURE_SHA256 is unset - the fixture is not being verified."; \ + exit 0; \ + fi; \ + 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 "fixture: missing sha256sum or shasum for checksum verification" >&2; exit 1; fi; \ + sha_of() { $$shacmd "$$1" | awk '{print $$1}'; }; \ + if [ "$$(sha_of "$(ETHREX_REAL_BLOCK_FIXTURE)")" = "$$want" ]; then exit 0; fi; \ + echo "fixture $(ETHREX_REAL_BLOCK_FIXTURE) does not match $$want - regenerating."; \ + $(MAKE) regen-real-block-fixture; \ + got="$$(sha_of "$(ETHREX_REAL_BLOCK_FIXTURE)")"; \ + if [ "$$got" != "$$want" ]; then \ + echo "ERROR: the freshly generated fixture is $$got, not the pinned $$want." >&2; \ + echo " The generator is deterministic, so this means its inputs or its output" >&2; \ + echo " format moved: the ethrex rev, the cache, or the generator itself. If that" >&2; \ + echo " was intended, re-baseline ETHREX_REAL_BLOCK_FIXTURE_SHA256 in the Makefile" >&2; \ + echo " and re-measure -- the workload is no longer the one every recorded number" >&2; \ + echo " was taken against." >&2; \ + exit 1; \ + fi # No prerequisites on purpose. `ethrex-real-block-cache` is phony (so its digest is # re-checked on every call), and a phony prerequisite always reads as newer than its From d1bf454405bc79d223cde433f3223e49754a5c94 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 9 Sep 2026 16:11:44 -0300 Subject: [PATCH 15/46] Re-baseline the rebuilt workload over 14 proves and correct what the benchmark harness publishes about its own resolving power. The five-prove figure it was pinned to, CV 0.57%, came with a 95% interval of [0.34%, 1.64%]; the fuller sample lands at 1.26%, near the top of that interval, so the pair table was promising roughly twice the resolution it can deliver -- 12 pairs resolve ~1.0%, not ~0.51%. The reason is not prover jitter. On this runner wall time and the share of CPU a prove actually gets correlate at -0.98 across a ten-prove sweep, perfectly monotonic: 7583% of a CPU takes 123.98 s and 7318% takes 129.61 s. Two proves that overlapped another job on the box got 4769% and 4687% and took 215.58 s and 220.03 s, +73%, while producing byte-identical proof bundles -- nothing but the clock moved. Gated to the proves that got the most CPU the same sweep has a CV of 0.34%, which is better than what was published. So the number was never wrong so much as conditional on something nobody recorded. bench_abba.sh now records the CPU share of every prove and prints an exclusivity line that flags any prove under 90% of the batch's best. The threshold is relative because the script also runs on rented 16-32 core GPU hosts where an absolute percentage means nothing, and it flags rather than discards because the ABBA pairing already absorbs most of the effect -- what a flag means is that the batch's spread must not be read as prover noise. The box is shared and everyone logs in as the same account, so contention cannot be attributed after the fact; it has to be recorded while measuring. The pair table now carries both conditions instead of bracketing an unknown, and the peak RSS figure moves from 43.00 GiB to the measured maximum of 44.81 GiB. That last one tightens an argument rather than weakening it: 47.0 GB is 98% of a 48 GB box, so the GPU arm's floor was already past the point of no headroom, not near it. --- .github/workflows/bench-abba.yml | 37 ++++++++++++---------- .github/workflows/benchmark-gpu.yml | 6 ++-- .github/workflows/benchmark-pr.yml | 2 +- scripts/bench_abba.sh | 49 +++++++++++++++++++++++++++-- tooling/ethrex-fixtures/README.md | 18 +++++++---- 5 files changed, 83 insertions(+), 29 deletions(-) diff --git a/.github/workflows/bench-abba.yml b/.github/workflows/bench-abba.yml index c691fd5b2..13db6f37c 100644 --- a/.github/workflows/bench-abba.yml +++ b/.github/workflows/bench-abba.yml @@ -9,24 +9,29 @@ name: Bench ABBA tiebreaker # Syntax: "/bench-abba [N]", where N is the pair count (default 12). # # Resolution, from the paired t-test (resolvable 95% delta = t* x sd / sqrt(N)). The -# pair-delta sd on this runner is NOT yet measured; the two columns bracket it between -# 0.6% and 0.81% (sqrt(2) x this runner's measured 0.57% single-run CV, from the -# 5-prove baseline in tooling/ethrex-fixtures/README.md): +# two columns are not a guess bracketing an unknown: they are the same runner under +# two conditions, because its variance is contention and not prover jitter. Over a +# 14-prove baseline (tooling/ethrex-fixtures/README.md) wall time and the share of CPU +# a prove actually got correlate at -0.98, perfectly monotonic, so the single-run CV is +# 0.34% across the proves that got the most CPU and 1.26% across all of them. sd of a +# pair delta is sqrt(2) x that: # -# pairs wall resolves (sd 0.81% / sd 0.6%) -# 8 ~41 min 0.67% / 0.50% -# 12 ~58 min 0.51% / 0.38% <- default -# 20 ~1h31m 0.38% / 0.28% -# 32 ~2h22m 0.29% / 0.22% +# pairs wall resolves (quiet box, sd 0.49% / shared, sd 1.78%) +# 8 ~41 min 0.34% / 1.24% +# 12 ~58 min 0.28% / 1.01% <- default +# 20 ~1h31m 0.21% / 0.78% +# 32 ~2h22m 0.16% / 0.62% # -# Wall is 2 x 125.17 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min -# of setup. The FIRST run measures the sd — it is the `sd` field of the paired-t line -# in the result comment — and this table should be re-pinned to it then. +# Wall is 2 x ~125 s per pair (the runner's measured prove at epoch 2^22) plus ~8 min of +# setup. Read the column the run earned, not the one you want: the script prints an +# exclusivity line reporting the CPU share of every prove and flagging any that came in +# under 90% of the batch's best. A flagged batch belongs in the right-hand column. # -# Both columns tightened when the workload was rebuilt for ethrex 25: the single-run -# CV fell from 1.43% to 0.57%, so 12 pairs now resolve ~0.5% where they used to resolve -# ~1.3%. Numbers from before that bump measured a different fixture (block 25368371 as -# an rkyv ProgramInput) and are not comparable. +# The rebuild for ethrex 25 did tighten this, but not as far as first recorded: a +# 5-prove baseline put the CV at 0.57%, and its 95% interval was [0.34%, 1.64%] — wide +# enough that the point estimate was not worth pinning a table to, which is what the +# 14-prove follow-up then showed. Numbers from before the bump measured a different +# fixture (block 25368371 as an rkyv ProgramInput) and are not comparable. on: issue_comment: types: [created] @@ -34,7 +39,7 @@ on: env: # The epoch /bench proves this block at, so the tiebreaker resolves the same # configuration and not just the same block. Memory picks it: this runner peaks at - # 43.00 GiB here against its >=64 GiB floor, and 2^23 would take that past 50 GiB + # 44.81 GiB here against its >=64 GiB floor, and 2^23 would take that past 50 GiB # for ~7% of wall. See benchmark-pr.yml's REAL_BLOCK_EPOCH_LOG2. ABBA_REAL_EPOCH_LOG2: "22" diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 2fec522bb..9961d0411 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -230,9 +230,9 @@ jobs: MIN_DRIVER: "580" run: | # cpu_ram filter is in GB. Floor 64 GB, raised from 48 with the ethrex 25 - # workload: that block peaks at 43.00 GiB (46.2 GB) at epoch 2^22 on the CPU - # path, which on a 48 GB box is 96% of RAM and an OOM waiting for a rental - # with a slightly hungrier allocator. 64 GB restores the ~28% headroom the + # workload: that block peaks at 44.81 GiB (47.0 GB) at epoch 2^22 on the CPU + # path, which on a 48 GB box is 98% of RAM -- not headroom to lose to a + # rental with a slightly hungrier allocator, it is already over. 64 GB restores the ~28% headroom the # 48 GB floor had for the old block (~36 GB host RSS on the CUDA path, # measured at main vintage) and matches the CPU runner's own floor. It costs # offer availability: fewer boxes qualify, so PRICE_CAP may need a look if diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 78e359c91..462bdaf6a 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -99,7 +99,7 @@ env: # predating that fix fails at write time rather than mismeasuring. # # Epoch 2^22, measured on THIS runner on 2026-09-08 (box idle): mean 125.17 s over - # 5 proves, sd 0.71 s, CV 0.57%, 43.00 GiB peak RSS, 790 MB bundle; verifying one + # 14 proves, sd 1.58 s, CV 1.26%, 44.81 GiB peak RSS, 790 MB bundle; verifying one # bundle is 12.0 s. # # Memory, not speed, is what picks 2^22. The epoch trade-off was swept on the diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index c8fd107a8..2966c1c79 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -242,11 +242,31 @@ else fi # --- 3. Interleaved A/B/B/A measurement (fresh CSV -- pre-committed batch) --- +# Every prove also records the share of CPU it actually got. On a shared box that +# is the difference between a number and a coincidence: measured on the bench +# runner, wall time and CPU share correlate at -0.98 across a ten-prove sweep, +# perfectly monotonic, and a colleague's job landing mid-sweep cost 47 of 75 cores +# and +73% of wall. So the spread this script reports is a property of how +# exclusive the box was, not of the prover -- gated to the runs that got the most +# CPU, the same sweep's CV falls from 1.54% to 0.34%. The ABBA pairing cancels most +# of it (both sides meet the same neighbours), which is why this flags rather than +# discards; a flagged batch is one whose spread should not be read as prover noise. +CPU_SHARES="$WORK/cpu_shares.txt"; : > "$CPU_SHARES" +TIME_BIN="" +if /usr/bin/time -f %P true >/dev/null 2>&1; then TIME_BIN=/usr/bin/time; fi + run_prove() { # $1=binary -> echoes proving time (s) - local out t + local out t share tf + tf="$(mktemp)" # shellcheck disable=SC2086 # CONT_ARGS is intentionally word-split (0 or 2 args) - out="$("$1" prove "$ELF" --private-input "$INPUT" -o "$PROOF" --time $CONT_ARGS 2>&1)" - rm -f "$PROOF" + if [ -n "$TIME_BIN" ]; then + out="$($TIME_BIN -f '%P' -o "$tf" "$1" prove "$ELF" --private-input "$INPUT" -o "$PROOF" --time $CONT_ARGS 2>&1)" + else + out="$("$1" prove "$ELF" --private-input "$INPUT" -o "$PROOF" --time $CONT_ARGS 2>&1)" + fi + share="$(tr -d '%' < "$tf" | tr -d '[:space:]')" + rm -f "$tf" "$PROOF" + case "$share" in ''|*[!0-9]*) : ;; *) echo "$share" >> "$CPU_SHARES" ;; esac t="$(printf '%s\n' "$out" | grep -o 'Proving time: [0-9.]*' | awk '{print $3}')" if [ -z "$t" ]; then echo "ERROR: could not parse 'Proving time' from cli output:" >&2 @@ -269,6 +289,29 @@ for i in $(seq 1 "$N_PAIRS"); do "$i" "$N_PAIRS" "$a" "$b" "$(awk "BEGIN{print ($a-$b)/$b*100}")" done +# Exclusivity report. Self-calibrating: the best prove of this batch defines what +# the box can give, so a run well under it met a neighbour. No box-specific +# constant, which matters because this script also runs on rented 16-32 core GPU +# hosts where an absolute percentage means nothing. +if [ -s "$CPU_SHARES" ]; then + awk ' + { n++; s[n]=$1; if ($1>mx) mx=$1; if (mn==0 || $1 Exclusivity: CPU share %d%%-%d%% of %d proves", mn, mx, n + if (bad) { + printf ", %d below 90%% of the best\n", bad + printf " Something else was on the box. The pairing absorbs most of it, but do\n" + printf " not read this batch spread as prover noise, and re-run on a quiet box\n" + printf " before quoting a resolvable delta.\n" + } else { + printf ", all within 10%% of the best\n" + } + }' "$CPU_SHARES" +fi + # --- 4. Paired t-test + robust median/Wilcoxon --- python3 - "$WORK/pairs.csv" <<'PY' import sys, csv, math diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 64ac08064..036ff1961 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -98,12 +98,13 @@ calls, 164 ECSM calls. Fixture: 549,144 bytes. ### Choosing the epoch size, and what the workload costs Measured on the bench runner (`vm-benchmarks-1`, which is also the CI -self-hosted `bench` runner: 96 cores / 125 GB, idle) on 2026-09-08, with the -guest ELF at ethrex `2cb18b0b`: +self-hosted `bench` runner: 96 cores / 125 GB) with the guest ELF at ethrex +`2cb18b0b`, over 14 proves across two sittings: | | mean | sd | CV | peak RSS | proof | verify | | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| epoch 2^22, 5 proves | **125.17 s** | 0.71 s | 0.57 % | 43.00 GiB | 790 MB | 12.0 s | +| epoch 2^22, 14 proves | **125.33 s** | 1.58 s | 1.26 % | 44.8 GiB | 790 MB | 12.0 s | +| of those, the 5 that got the most CPU | 124.47 s | 0.43 s | 0.34 % | | | | 2^22 is what `/bench`, `/bench-abba` and the GPU bench pin. The epoch trade-off itself was swept on the previous block (2^21 costs +13.7 % of wall to save @@ -112,9 +113,14 @@ than by the block, so that shape carries over even though the seconds do not. 2^23 would take this workload past 50 GiB against the runner's 64 GiB floor, which is why memory and not speed picks the default. -With that sd, a two-sided 95 % comparison resolves ~2.00 % at three runs per -side, ~1.00 % at five and ~0.58 % at ten — which is why a sub-2 % claim needs -`/bench N` or the ABBA tiebreaker rather than a re-read of a three-run table. +The two rows are the same binary on the same block; what separates them is how +much of the shared box each prove got. Wall time here is a function of CPU share, +not of the prover, so quote a spread only together with the condition it was +measured under — `scripts/bench_abba.sh` records the CPU share of every prove and +flags a contended batch, and its comments carry the measurement. A two-sided 95 % +comparison resolves ~0.6 % at three runs per side on a quiet box and ~2.0 % on a +busy one, which is why a sub-2 % claim needs `/bench N` or the ABBA tiebreaker +rather than a re-read of a three-run table. Continuations are not optional here: monolithic proving costs ~4.9 GB of peak heap per million cycles on this family, so 37.14M cycles would need ~182 GB. From e78c0032d3cd4db446249fdd20dabe9ccbeff35d Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 12:23:02 -0300 Subject: [PATCH 16/46] Pin ethrex 26.0.0 and fix the fixture regen --- .github/workflows/bench-abba.yml | 2 +- .github/workflows/benchmark-gpu.yml | 2 +- .github/workflows/benchmark-pr.yml | 4 +- .github/workflows/ethrex-block-converter.yml | 2 +- Makefile | 2 +- crypto/ethrex-crypto/Cargo.lock | 4 +- crypto/ethrex-crypto/Cargo.toml | 2 +- executor/programs/rust/ethrex/Cargo.lock | 32 +++++----- executor/programs/rust/ethrex/Cargo.toml | 2 +- executor/tests/README.md | 6 +- tooling/ethrex-block-converter/Cargo.lock | 52 ++++++++-------- tooling/ethrex-block-converter/Cargo.toml | 10 ++-- tooling/ethrex-block-converter/README.md | 6 +- tooling/ethrex-block-converter/src/main.rs | 2 +- tooling/ethrex-fixtures/Cargo.lock | 60 +++++++++---------- tooling/ethrex-fixtures/Cargo.toml | 20 ++++--- tooling/ethrex-fixtures/README.md | 5 +- tooling/ethrex-fixtures/src/bin/real_block.rs | 2 +- tooling/ethrex-tests/Cargo.lock | 32 +++++----- tooling/ethrex-tests/Cargo.toml | 2 +- 20 files changed, 129 insertions(+), 120 deletions(-) diff --git a/.github/workflows/bench-abba.yml b/.github/workflows/bench-abba.yml index 13db6f37c..ede04cadc 100644 --- a/.github/workflows/bench-abba.yml +++ b/.github/workflows/bench-abba.yml @@ -27,7 +27,7 @@ name: Bench ABBA tiebreaker # exclusivity line reporting the CPU share of every prove and flagging any that came in # under 90% of the batch's best. A flagged batch belongs in the right-hand column. # -# The rebuild for ethrex 25 did tighten this, but not as far as first recorded: a +# The workload rebuild did tighten this, but not as far as first recorded: a # 5-prove baseline put the CV at 0.57%, and its 95% interval was [0.34%, 1.64%] — wide # enough that the point estimate was not worth pinning a table to, which is what the # 14-prove follow-up then showed. Numbers from before the bump measured a different diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 9961d0411..1f715d45a 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -229,7 +229,7 @@ jobs: # because vast can't numerically compare the driver_version string server-side. MIN_DRIVER: "580" run: | - # cpu_ram filter is in GB. Floor 64 GB, raised from 48 with the ethrex 25 + # cpu_ram filter is in GB. Floor 64 GB, raised from 48 with the rebuilt # workload: that block peaks at 44.81 GiB (47.0 GB) at epoch 2^22 on the CPU # path, which on a 48 GB box is 98% of RAM -- not headroom to lose to a # rental with a slightly hungrier allocator, it is already over. 64 GB restores the ~28% headroom the diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 58634404d..4d8152dd6 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -113,7 +113,7 @@ env: # # The pre-bump numbers this replaces (block 25368371, 50.78M cycles, 158.8 s at # 2^22 on this runner, ~52 GB) measured a DIFFERENT workload: that fixture was an - # rkyv ProgramInput, which ethrex 25's guest rejects. Do not compare across the + # rkyv ProgramInput, which the pinned guest rejects. Do not compare across the # bump. This block was picked to land as close to it as an Amsterdam rebuild can — # see the screen in the Makefile's ETHREX_REAL_BLOCK comment. # @@ -315,7 +315,7 @@ jobs: if: steps.config.outputs.run_real == 'true' run: | # Gitignored and never in the checkout, like the synthetic fixtures above. - # Built, not downloaded: ethrex 25's guest decodes only the Amsterdam schema, + # Built, not downloaded: the pinned guest decodes only the Amsterdam schema, # so a hosted artifact for this pre-Amsterdam block cannot be valid — it would # run ~500 cycles and commit `successful_validation = 0` without failing. The # generator rebuilds the block's real transactions on its real pre-state and diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index f8ca2a2ff..8a97a335d 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -86,7 +86,7 @@ jobs: tooling/ethrex-fixtures -> target tooling/ethrex-tests -> target - # Built from the block's replay cache, not downloaded: ethrex 25's guest decodes + # Built from the block's replay cache, not downloaded: the pinned guest decodes # only the Amsterdam schema, so no hosted artifact for a pre-Amsterdam block can # be valid. The generator validates the block through the guest before writing, # which is what the screen below then re-checks against the ELF. The guard covers diff --git a/Makefile b/Makefile index 48509816f..2dfc12277 100644 --- a/Makefile +++ b/Makefile @@ -316,7 +316,7 @@ ETHREX_REAL_BLOCK := 25453112 # not a selection criterion — every pre-Amsterdam block loses 26-50% of its # transactions to the fork, which is a property of the fork and not of the block. # -# The fixture is GENERATED from the cache below, not fetched. ethrex 25's guest +# The fixture is GENERATED from the cache below, not fetched. The pinned guest # decodes only the Amsterdam schema (`0x1501`) and mainnet has no Amsterdam fork, # so no hosted artifact for this block can be valid: the release's rkyv one now # runs 496 cycles and commits `successful_validation = 0` instead of failing. diff --git a/crypto/ethrex-crypto/Cargo.lock b/crypto/ethrex-crypto/Cargo.lock index 67a36c419..44c941387 100644 --- a/crypto/ethrex-crypto/Cargo.lock +++ b/crypto/ethrex-crypto/Cargo.lock @@ -342,8 +342,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ark-bn254", "ark-ec", diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml index 774dd2cf4..a3377a32e 100644 --- a/crypto/ethrex-crypto/Cargo.toml +++ b/crypto/ethrex-crypto/Cargo.toml @@ -19,7 +19,7 @@ license = "MIT OR Apache-2.0" # + `default-features = false` as the guest's ethrex-crypto, so feature # unification adds nothing to the guest build (no C secp256k1 / malachite / kzg). # `std` is on anyway, via ethrex-trie's default. -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-crypto", default-features = false } # Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 # (a version split would make `FieldElement`/`Scalar` incompatible types). # `expose-field` is required by the x-only reconstruction. diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index 14f929569..441cd6fde 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -667,8 +667,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crc32fast", @@ -699,8 +699,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ark-bn254", "ark-ec", @@ -719,8 +719,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -742,8 +742,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -760,8 +760,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", @@ -778,8 +778,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -788,8 +788,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -808,8 +808,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 65930a94f..06cae1ae2 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -55,7 +55,7 @@ lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # # KZG is not linked, so the point-evaluation precompile (0x0a) is unsupported — # see src/main.rs. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program", default-features = false } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program", default-features = false } # `lambda-vm-syscalls` can still reach the graph as a git dep pinned to a commit # of THIS repo, which upstream bumps on its own schedule. Override it # with the working-tree copy so the guest always links the same syscalls as the rest diff --git a/executor/tests/README.md b/executor/tests/README.md index ed5e0148c..12309f39a 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -10,14 +10,14 @@ commit: ```text https://github.com/lambdaclass/ethrex.git -2cb18b0b95b27a2555d3debffdebc43c9685d6e3 +8effcb0671c5d0b12fe0161ea37c174ec4466b6a ``` Five manifests carry that pin, not one. `scripts/set_ethrex_rev.sh --show` prints it and fails if they ever disagree. The generator enables Amsterdam in its synthetic genesis and includes the two -EIP-8282 request predeploys required by ethrex 25. +EIP-8282 request predeploys the pinned ethrex requires. ### Generation @@ -75,7 +75,7 @@ ethrex_bench_4.bin The blocks above are synthetic. For a representative workload — real contract execution, real trie depth, real bytecode — `make ethrex-real-block-fixture` BUILDS `ethrex_mainnet_25453112.bin` from the block's replay cache, which is the -only fetched artifact; nothing about the fixture is published, because ethrex 25's +only fetched artifact; nothing about the fixture is published, because the pinned guest decodes only the Amsterdam schema and no hosted artifact for a pre-Amsterdam block can be valid. It is gitignored rather than committed, so its digest lives next to the block pin in the Makefile rather than in the table above, and it is diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index ffa6e7224..057713c5c 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -866,8 +866,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crossbeam", @@ -891,8 +891,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crc32fast", @@ -924,8 +924,8 @@ dependencies = [ [[package]] name = "ethrex-config" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ethrex-common", "ethrex-p2p", @@ -936,8 +936,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ark-bn254", "ark-ec", @@ -958,8 +958,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -981,8 +981,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -999,8 +999,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", @@ -1017,8 +1017,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ethrex-common", "serde", @@ -1029,8 +1029,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "aes", "aes-gcm", @@ -1071,8 +1071,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -1081,8 +1081,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -1103,8 +1103,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -1123,8 +1123,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 4c546d28a..04304554e 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -9,14 +9,14 @@ edition = "2024" [dependencies] # Pinned to the SAME ethrex rev as the guest so the generated SSZ input matches # what the stateless guest deserializes. -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-common", default-features = false, features = ["secp256k1"] } -ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-config", default-features = false } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common", default-features = false, features = ["secp256k1"] } +ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-config", default-features = false } # Provides `NativeCrypto`, the host `Crypto` implementation used to recover # transaction keys, hash the BAL, and validate the generated stateless input. # Already in the graph via `ethrex-config` -> `ethrex-p2p`, so this adds no # compilation; the line exists because the bin build needs its own. -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-crypto", default-features = false, features = ["secp256k1"] } -ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-rlp" } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-crypto", default-features = false, features = ["secp256k1"] } +ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-rlp" } # 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 @@ -27,7 +27,7 @@ ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0 # verify with `cargo tree -e features -i ethrex-crypto`. c-kzg is the whole gap now — # malachite modexp and `ark-ff/asm` are on both sides since ethrex-trie's `std` default. # Closing it means dropping `ethrex-config` and sourcing `ChainConfig` another way. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program", default-features = false } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program", default-features = false } # Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. libssz = "=0.3.0" diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 2b29b837f..2f6d766a2 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -14,7 +14,7 @@ cargo run --release -- ``` The output starts with the two-byte big-endian schema ID `0x1501`. The current -ethrex 25 stateless schema validates one Amsterdam block at a time. A replay +pinned stateless schema validates one Amsterdam block at a time. A replay cache must therefore contain one block, its `slot_number`, its `block_access_list_hash`, and the raw BAL when the BAL is non-empty. Caches created before Amsterdam are rejected rather than silently rewriting their @@ -30,7 +30,7 @@ The converter, fixture generator, guest, and host tests all pin: ```text https://github.com/lambdaclass/ethrex.git -2cb18b0b95b27a2555d3debffdebc43c9685d6e3 +8effcb0671c5d0b12fe0161ea37c174ec4466b6a ``` Keep these pins together. The SSZ wire format and the guest implementation are @@ -54,7 +54,7 @@ sha256sum "$(make -s print-real-block-fixture)" Publish the resulting SSZ artifact and matching Amsterdam cache, then update their URLs and checksums in the Makefile before enabling the real-block benchmark. The old release assets are rkyv `ProgramInput` artifacts and are -incompatible with ethrex 25. +incompatible with the pinned ethrex. The converter's tests use the Hoodi cache under `caches/` and verify that an unmapped network is rejected. The checked release cache predates Amsterdam, so diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index a3809c806..7f5bdfd0f 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -108,7 +108,7 @@ fn stateless_input_from_cache( }; if cache.blocks.len() != 1 { return Err(format!( - "cache contains {} blocks; the ethrex 25 stateless guest accepts one block", + "cache contains {} blocks; the pinned stateless guest accepts one block", cache.blocks.len() ) .into()); diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index bdd48518e..d755840ac 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -975,8 +975,8 @@ dependencies = [ [[package]] name = "ethrex-blockchain" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crossbeam", @@ -1000,8 +1000,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crc32fast", @@ -1034,8 +1034,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ark-bn254", "ark-ec", @@ -1076,8 +1076,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -1099,8 +1099,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -1117,8 +1117,8 @@ dependencies = [ [[package]] name = "ethrex-l2-rpc" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "axum", "bytes", @@ -1149,8 +1149,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", @@ -1168,8 +1168,8 @@ dependencies = [ [[package]] name = "ethrex-metrics" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "axum", "ethrex-common", @@ -1184,8 +1184,8 @@ dependencies = [ [[package]] name = "ethrex-p2p" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "aes", "aes-gcm", @@ -1228,8 +1228,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -1238,8 +1238,8 @@ dependencies = [ [[package]] name = "ethrex-rpc" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "axum", "axum-extra", @@ -1278,8 +1278,8 @@ dependencies = [ [[package]] name = "ethrex-storage" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -1300,8 +1300,8 @@ dependencies = [ [[package]] name = "ethrex-storage-rollup" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "async-trait", "bincode", @@ -1316,8 +1316,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -1336,8 +1336,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index 1b5bd3249..ce79d4c0d 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -2,6 +2,12 @@ name = "ethrex-fixtures" version = "0.1.0" edition = "2024" +# This crate grew a second binary (`real_block`), which made a bare `cargo run` +# ambiguous and broke `make regen-ethrex-fixtures` plus the commands the fixture +# READMEs tell you to run on a rev bump. The synthetic generator is the one that +# should answer to a bare `cargo run`; the real-block rebuild is always explicit +# (`--bin real_block`, as the Makefile invokes it). +default-run = "ethrex-fixtures" # Detached workspace: keeps the heavy ethrex host deps out of the main build. [workspace] @@ -10,15 +16,15 @@ edition = "2024" # Pinned to the SAME ethrex rev as the guest so generated SSZ inputs match what # the stateless guest deserializes. Re-pin every manifest together with # scripts/set_ethrex_rev.sh. -ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-blockchain" } -ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-storage" } -ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-common" } -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program" } -ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-l2-rpc" } -ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-rlp" } +ethrex-blockchain = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-blockchain" } +ethrex-storage = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-storage" } +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program" } +ethrex-l2-rpc = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-l2-rpc" } +ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-rlp" } # Walking the witness tries: the storage keys are `keccak(slot)`, so recovering a # genesis-shaped alloc from a witness needs the node types, not just the accessors. -ethrex-trie = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-trie" } +ethrex-trie = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-trie" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 1cd68fecd..b0a7c1e09 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -93,7 +93,10 @@ EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the repository root has both sets measured side by side. Measured on the guest ELF at ethrex `2cb18b0b`: 37,137,386 cycles, 6,003 keccak -calls, 164 ECSM calls. Fixture: 549,144 bytes. +calls, 164 ECSM calls. Fixture: 549,144 bytes. The pin has since moved to +`8effcb06`, which declares 26.0.0 and whose tree is byte-identical in every crate +this repo compiles, so the counts carry — and the fixture regenerates to the same +digest at both. **Pin the ELF whenever you quote a cycle count.** Those three counts are deterministic for a given ELF and input, and they move with anything that changes diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index f9328f9b0..a2e19cddf 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -1,7 +1,7 @@ //! Rebuild a real mainnet block as an Amsterdam block the stateless guest accepts. //! //! The retired benchmark fixture (`ethrex_mainnet_25368371.bin`) is an rkyv -//! `ProgramInput` from before execution-specs #3278. ethrex 25's guest only +//! `ProgramInput` from before execution-specs #3278. The pinned guest only //! decodes schema `0x1501` (Amsterdam) and mainnet has no Amsterdam fork yet, so //! that block cannot be re-serialized: it has no block access list hash and no //! slot number, and its state carries none of the EIP-8282 predeploys Amsterdam diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 62e8cd38c..4a27fae68 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -693,8 +693,8 @@ dependencies = [ [[package]] name = "ethrex-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "crc32fast", @@ -726,8 +726,8 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "ark-bn254", "ark-ec", @@ -747,8 +747,8 @@ dependencies = [ [[package]] name = "ethrex-guest-program" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -770,8 +770,8 @@ dependencies = [ [[package]] name = "ethrex-l2-common" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -788,8 +788,8 @@ dependencies = [ [[package]] name = "ethrex-levm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", @@ -806,8 +806,8 @@ dependencies = [ [[package]] name = "ethrex-rlp" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "ethereum-types", @@ -825,8 +825,8 @@ dependencies = [ [[package]] name = "ethrex-trie" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "anyhow", "bytes", @@ -845,8 +845,8 @@ dependencies = [ [[package]] name = "ethrex-vm" -version = "25.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=2cb18b0b95b27a2555d3debffdebc43c9685d6e3#2cb18b0b95b27a2555d3debffdebc43c9685d6e3" +version = "26.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=8effcb0671c5d0b12fe0161ea37c174ec4466b6a#8effcb0671c5d0b12fe0161ea37c174ec4466b6a" dependencies = [ "bytes", "derive_more", diff --git a/tooling/ethrex-tests/Cargo.toml b/tooling/ethrex-tests/Cargo.toml index a6e085f06..9912b7dd7 100644 --- a/tooling/ethrex-tests/Cargo.toml +++ b/tooling/ethrex-tests/Cargo.toml @@ -12,7 +12,7 @@ executor = { path = "../../executor" } # Pinned to the SAME ethrex rev as the guest so the native reference reads the # same SSZ stateless input and output. Re-pin every manifest together with # scripts/set_ethrex_rev.sh. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "2cb18b0b95b27a2555d3debffdebc43c9685d6e3", package = "ethrex-guest-program" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program" } libssz = "=0.3.0" # Match the root workspace's optimized dev profile: these tests execute the VM. From f0a4121e195290624fc6a7f2616df84456c34b8d Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 15:22:44 -0300 Subject: [PATCH 17/46] Re-measure the workload at the new pin --- .github/workflows/benchmark-pr.yml | 2 +- tooling/ethrex-fixtures/README.md | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 4d8152dd6..b96e938f1 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -70,7 +70,7 @@ env: ELF: executor/program_artifacts/rust/ethrex.elf # 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 37,137,386 + # moves this job without editing it. At the current default that is 37,137,748 # cycles, 6,003 keccak calls and 164 ecsm calls: 13.6x the work of the synthetic # block (ethrex_bench_4, 2,727,681 cycles) with a keccak:ecrecover mix of 37:1 # against its 13:1. That is the whole point — a prover change can move the diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index b0a7c1e09..d05e18dca 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -92,19 +92,27 @@ economics. For workloads whose gas limits were computed for Amsterdam, use the EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the repository root has both sets measured side by side. -Measured on the guest ELF at ethrex `2cb18b0b`: 37,137,386 cycles, 6,003 keccak -calls, 164 ECSM calls. Fixture: 549,144 bytes. The pin has since moved to -`8effcb06`, which declares 26.0.0 and whose tree is byte-identical in every crate -this repo compiles, so the counts carry — and the fixture regenerates to the same -digest at both. +Measured on the guest ELF at ethrex `8effcb06`: **37,137,748 cycles**, 6,003 +keccak calls, 164 ECSM calls. Fixture: 549,144 bytes. -**Pin the ELF whenever you quote a cycle count.** Those three counts are +**Pin the ELF whenever you quote a cycle count.** The three counts are deterministic for a given ELF and input, and they move with anything that changes -the guest: the ethrex rev, guest optimisation flags, and the **clang major -version** by around 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 — two boxes with different clang majors report different counts for the same -commit. A count without its ELF is not comparable to anything. +the guest — including changes that touch none of the source it compiles. Moving +the pin from `2cb18b0b` to `8effcb06` cost **+362 cycles, 0.001 %**, with keccak +and ECSM identical, and that was the whole difference: no `.rs` file in the +guest's dependency graph differs between the two revs and no crates.io dependency +moved, so what shifted is the version metadata the ethrex crates carry. Immaterial +next to the ~1 % this workload can resolve, but it does mean an exact count +belongs to an exact rev. + +The compiler is not pinned either — the guest embeds C (`secp256k1-sys`) and the +Makefile pins target flags but not `cc` — so in principle two boxes with different +clang majors can disagree. In practice the effect is small and not well +characterised: the one figure recorded in this repo is 0.13 % on a different +block, while this block came out at exactly 37,137,386 on both macOS/arm64 and the +Linux x86-64 runner at the previous pin, from ELF binaries that were themselves +different. Quote the rev; do not assume the machine matters, and do not assume it +does not. ### Choosing the epoch size, and what the workload costs From c46a1c0780e75794fdcab94ca016cbf980dd9062 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:31:57 -0300 Subject: [PATCH 18/46] Share the SSZ input builder between both generators `build_stateless_input` and `empty_execution_requests` existed as two near-identical copies, one per binary in this crate, differing only in how the block type was spelled. The file that carried the second copy said a real PR should lift them into a module, and this is that PR. They now live in src/lib.rs and both binaries import them, so the next ethrex rev bump moves one encoder instead of two that nothing forced to stay in step. Behaviour-preserving, and checked that way rather than assumed: after the move all five generated fixtures still come out at the digests they already had, the real-block one byte-for-byte at 08a52e10 with the same installed state root. --- tooling/ethrex-fixtures/src/bin/real_block.rs | 146 +---------------- tooling/ethrex-fixtures/src/lib.rs | 155 ++++++++++++++++++ tooling/ethrex-fixtures/src/main.rs | 146 +---------------- 3 files changed, 157 insertions(+), 290 deletions(-) create mode 100644 tooling/ethrex-fixtures/src/lib.rs diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index a2e19cddf..5f382cdb8 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -26,23 +26,17 @@ use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; use ethrex_blockchain::{Blockchain, BlockchainOptions}; use ethrex_common::H256; -use ethrex_common::types::block_access_list::BlockAccessList; use ethrex_common::types::block_execution_witness::{ ExecutionWitness, RpcExecutionWitness, amsterdam_chain_config, decode_witness_headers, }; -use ethrex_common::types::stateless_ssz::{ - Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, - STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, -}; use ethrex_common::types::{AccountState, Block, ELASTICITY_MULTIPLIER, Genesis}; +use ethrex_fixtures::build_stateless_input; use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; use ethrex_guest_program::l1::run_stateless_guest; use ethrex_rlp::decode::RLPDecode; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::{EngineType, Store}; use ethrex_trie::{Nibbles, Node, NodeRef}; -use libssz::SszEncode; -use libssz_types::{ProgressiveList, SszList, SszVector}; use std::collections::BTreeMap; const GENESIS_JSON: &str = include_str!("../../genesis.json"); @@ -55,144 +49,6 @@ const CHAIN_ID: u64 = 1; const GENESIS_BASE_FEE: u64 = 1_000; const GAS_CEIL: u64 = 60_000_000; -fn empty_execution_requests() -> ExecutionRequests { - ExecutionRequests { - deposits: ProgressiveList::new(), - withdrawals: ProgressiveList::new(), - consolidations: ProgressiveList::new(), - builder_deposits: ProgressiveList::new(), - builder_exits: ProgressiveList::new(), - } -} - -fn build_stateless_input( - block: &Block, - witness: &RpcExecutionWitness, - block_access_list: Option<&BlockAccessList>, - chain_id: u64, -) -> Result, Box> { - let header = &block.header; - let bal = block_access_list.ok_or("Amsterdam fixture has no block access list")?; - let block_access_list_hash = header - .block_access_list_hash - .ok_or("Amsterdam fixture header has no block_access_list_hash")?; - if bal.compute_hash(&NativeCrypto) != block_access_list_hash { - return Err("fixture BAL does not match block_access_list_hash".into()); - } - let slot_number = header - .slot_number - .ok_or("Amsterdam fixture header has no slot_number")?; - - let transactions = block - .body - .transactions - .iter() - .map(|tx| tx.encode_canonical_to_vec().into()) - .collect::>>() - .into(); - let public_keys = block - .body - .transactions - .iter() - .enumerate() - .map(|(i, tx)| { - let key = tx - .public_key(&NativeCrypto) - .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? - .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; - SszVector::try_from(key.to_vec()) - .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) - }) - .collect::, String>>()?; - let withdrawals = block - .body - .withdrawals - .clone() - .unwrap_or_default() - .into_iter() - .map( - |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { - index: withdrawal.index, - validator_index: withdrawal.validator_index, - address: Bytes20(withdrawal.address.0), - amount: withdrawal.amount, - }, - ) - .collect::>() - .into(); - let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec())?; - let extra_data = SszList::try_from(header.extra_data.to_vec())?; - let base_fee = header.base_fee_per_gas.ok_or("fixture has no base fee")?; - let mut base_fee_bytes = [0u8; 32]; - base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); - - let execution_payload = ExecutionPayload { - parent_hash: header.parent_hash.0, - fee_recipient: Bytes20(header.coinbase.0), - state_root: header.state_root.0, - receipts_root: header.receipts_root.0, - logs_bloom, - prev_randao: header.prev_randao.0, - block_number: header.number, - gas_limit: header.gas_limit, - gas_used: header.gas_used, - timestamp: header.timestamp, - extra_data, - base_fee_per_gas: base_fee_bytes, - block_hash: header.compute_block_hash(&NativeCrypto).0, - transactions, - withdrawals, - blob_gas_used: header.blob_gas_used.unwrap_or_default(), - excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), - block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(bal).into(), - slot_number, - }; - let ssz_witness = SszExecutionWitness { - state: ProgressiveList::from( - witness - .state - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - ), - codes: ProgressiveList::from( - witness - .codes - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - ), - headers: SszList::try_from( - witness - .headers - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - )?, - }; - let input = SszStatelessInput { - new_payload_request: NewPayloadRequest { - execution_payload, - versioned_hashes: block - .body - .transactions - .iter() - .flat_map(|tx| tx.blob_versioned_hashes()) - .map(|hash| hash.0) - .collect::>() - .into(), - parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, - execution_requests: empty_execution_requests(), - }, - witness: ssz_witness, - chain_id, - public_keys: SszPublicKeys::from(public_keys), - }; - let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); - input.ssz_append(&mut bytes); - Ok(bytes) -} - /// Walk an embedded witness trie and return every `(32-byte key, value)` leaf it /// still holds. Pruned subtrees appear as `NodeRef::Hash` and are skipped: they /// are the proof siblings, which no transaction in the block reads. diff --git a/tooling/ethrex-fixtures/src/lib.rs b/tooling/ethrex-fixtures/src/lib.rs new file mode 100644 index 000000000..00849e20f --- /dev/null +++ b/tooling/ethrex-fixtures/src/lib.rs @@ -0,0 +1,155 @@ +//! Shared SSZ input construction for this crate's two generators. +//! +//! `build_stateless_input` is the encoder both binaries need — `ethrex-fixtures` for the +//! synthetic blocks it builds from a funded genesis, and `real_block` for a real mainnet +//! block rebuilt as an Amsterdam one. It used to be copied into each, which meant the +//! copies had to be moved together on every ethrex rev bump with nothing enforcing it. + +use ethrex_common::types::Block; +use ethrex_common::types::block_access_list::BlockAccessList; +use ethrex_common::types::block_execution_witness::RpcExecutionWitness; +use ethrex_common::types::stateless_ssz::{ + Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, + STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, +}; +use ethrex_guest_program::crypto::NativeCrypto; +use libssz::SszEncode; +use libssz_types::{ProgressiveList, SszList, SszVector}; + +pub fn empty_execution_requests() -> ExecutionRequests { + ExecutionRequests { + deposits: ProgressiveList::new(), + withdrawals: ProgressiveList::new(), + consolidations: ProgressiveList::new(), + builder_deposits: ProgressiveList::new(), + builder_exits: ProgressiveList::new(), + } +} + +pub fn build_stateless_input( + block: &Block, + witness: &RpcExecutionWitness, + block_access_list: Option<&BlockAccessList>, + chain_id: u64, +) -> Result, Box> { + let header = &block.header; + let bal = block_access_list.ok_or("Amsterdam fixture has no block access list")?; + let block_access_list_hash = header + .block_access_list_hash + .ok_or("Amsterdam fixture header has no block_access_list_hash")?; + if bal.compute_hash(&NativeCrypto) != block_access_list_hash { + return Err("fixture BAL does not match block_access_list_hash".into()); + } + let slot_number = header + .slot_number + .ok_or("Amsterdam fixture header has no slot_number")?; + + let transactions = block + .body + .transactions + .iter() + .map(|tx| tx.encode_canonical_to_vec().into()) + .collect::>>() + .into(); + let public_keys = block + .body + .transactions + .iter() + .enumerate() + .map(|(i, tx)| { + let key = tx + .public_key(&NativeCrypto) + .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? + .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; + SszVector::try_from(key.to_vec()) + .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) + }) + .collect::, String>>()?; + let withdrawals = block + .body + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map( + |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: Bytes20(withdrawal.address.0), + amount: withdrawal.amount, + }, + ) + .collect::>() + .into(); + let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec())?; + let extra_data = SszList::try_from(header.extra_data.to_vec())?; + let base_fee = header.base_fee_per_gas.ok_or("fixture has no base fee")?; + let mut base_fee_bytes = [0u8; 32]; + base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); + + let execution_payload = ExecutionPayload { + parent_hash: header.parent_hash.0, + fee_recipient: Bytes20(header.coinbase.0), + state_root: header.state_root.0, + receipts_root: header.receipts_root.0, + logs_bloom, + prev_randao: header.prev_randao.0, + block_number: header.number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data, + base_fee_per_gas: base_fee_bytes, + block_hash: header.compute_block_hash(&NativeCrypto).0, + transactions, + withdrawals, + blob_gas_used: header.blob_gas_used.unwrap_or_default(), + excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), + block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(bal).into(), + slot_number, + }; + let ssz_witness = SszExecutionWitness { + state: ProgressiveList::from( + witness + .state + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + codes: ProgressiveList::from( + witness + .codes + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + ), + headers: SszList::try_from( + witness + .headers + .iter() + .map(|bytes| SszList::try_from(bytes.to_vec())) + .collect::, _>>()?, + )?, + }; + let input = SszStatelessInput { + new_payload_request: NewPayloadRequest { + execution_payload, + versioned_hashes: block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .map(|hash| hash.0) + .collect::>() + .into(), + parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, + execution_requests: empty_execution_requests(), + }, + witness: ssz_witness, + chain_id, + public_keys: SszPublicKeys::from(public_keys), + }; + let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); + input.ssz_append(&mut bytes); + Ok(bytes) +} diff --git a/tooling/ethrex-fixtures/src/main.rs b/tooling/ethrex-fixtures/src/main.rs index 4875fde77..e91ac0e6a 100644 --- a/tooling/ethrex-fixtures/src/main.rs +++ b/tooling/ethrex-fixtures/src/main.rs @@ -24,22 +24,16 @@ use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; use ethrex_blockchain::{Blockchain, BlockchainOptions}; -use ethrex_common::types::block_access_list::BlockAccessList; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; -use ethrex_common::types::stateless_ssz::{ - Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, - STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, -}; use ethrex_common::types::{ EIP1559Transaction, ELASTICITY_MULTIPLIER, Genesis, GenesisAccount, Transaction, TxKind, }; use ethrex_common::{Address, H256, U256}; +use ethrex_fixtures::build_stateless_input; use ethrex_guest_program::crypto::NativeCrypto; use ethrex_guest_program::l1::run_stateless_guest; use ethrex_l2_rpc::signer::{LocalSigner, Signable, Signer}; use ethrex_storage::{EngineType, Store}; -use libssz::SszEncode; -use libssz_types::{ProgressiveList, SszList, SszVector}; use secp256k1::SecretKey; /// Well-known load-test rich account (funded in genesis.json). Key is public @@ -85,144 +79,6 @@ fn rich_signer() -> Result> { Ok(LocalSigner::new(SecretKey::from_slice(&hex::decode(RICH_PK)?)?).into()) } -fn empty_execution_requests() -> ExecutionRequests { - ExecutionRequests { - deposits: ProgressiveList::new(), - withdrawals: ProgressiveList::new(), - consolidations: ProgressiveList::new(), - builder_deposits: ProgressiveList::new(), - builder_exits: ProgressiveList::new(), - } -} - -fn build_stateless_input( - block: ðrex_common::types::Block, - witness: &RpcExecutionWitness, - block_access_list: Option<&BlockAccessList>, - chain_id: u64, -) -> Result, Box> { - let header = &block.header; - let bal = block_access_list.ok_or("Amsterdam fixture has no block access list")?; - let block_access_list_hash = header - .block_access_list_hash - .ok_or("Amsterdam fixture header has no block_access_list_hash")?; - if bal.compute_hash(&NativeCrypto) != block_access_list_hash { - return Err("fixture BAL does not match block_access_list_hash".into()); - } - let slot_number = header - .slot_number - .ok_or("Amsterdam fixture header has no slot_number")?; - - let transactions = block - .body - .transactions - .iter() - .map(|tx| tx.encode_canonical_to_vec().into()) - .collect::>>() - .into(); - let public_keys = block - .body - .transactions - .iter() - .enumerate() - .map(|(i, tx)| { - let key = tx - .public_key(&NativeCrypto) - .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? - .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; - SszVector::try_from(key.to_vec()) - .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) - }) - .collect::, String>>()?; - let withdrawals = block - .body - .withdrawals - .clone() - .unwrap_or_default() - .into_iter() - .map( - |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { - index: withdrawal.index, - validator_index: withdrawal.validator_index, - address: Bytes20(withdrawal.address.0), - amount: withdrawal.amount, - }, - ) - .collect::>() - .into(); - let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec())?; - let extra_data = SszList::try_from(header.extra_data.to_vec())?; - let base_fee = header.base_fee_per_gas.ok_or("fixture has no base fee")?; - let mut base_fee_bytes = [0u8; 32]; - base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); - - let execution_payload = ExecutionPayload { - parent_hash: header.parent_hash.0, - fee_recipient: Bytes20(header.coinbase.0), - state_root: header.state_root.0, - receipts_root: header.receipts_root.0, - logs_bloom, - prev_randao: header.prev_randao.0, - block_number: header.number, - gas_limit: header.gas_limit, - gas_used: header.gas_used, - timestamp: header.timestamp, - extra_data, - base_fee_per_gas: base_fee_bytes, - block_hash: header.compute_block_hash(&NativeCrypto).0, - transactions, - withdrawals, - blob_gas_used: header.blob_gas_used.unwrap_or_default(), - excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), - block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(bal).into(), - slot_number, - }; - let ssz_witness = SszExecutionWitness { - state: ProgressiveList::from( - witness - .state - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - ), - codes: ProgressiveList::from( - witness - .codes - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - ), - headers: SszList::try_from( - witness - .headers - .iter() - .map(|bytes| SszList::try_from(bytes.to_vec())) - .collect::, _>>()?, - )?, - }; - let input = SszStatelessInput { - new_payload_request: NewPayloadRequest { - execution_payload, - versioned_hashes: block - .body - .transactions - .iter() - .flat_map(|tx| tx.blob_versioned_hashes()) - .map(|hash| hash.0) - .collect::>() - .into(), - parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, - execution_requests: empty_execution_requests(), - }, - witness: ssz_witness, - chain_id, - public_keys: SszPublicKeys::from(public_keys), - }; - let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); - input.ssz_append(&mut bytes); - Ok(bytes) -} - /// Distinct recipient address for tx index `i` (fresh account, no funding needed). fn recipient_for(i: u64) -> Address { Address::from_low_u64_be(0xdead_0000_0000u64 + i) From 2609d8c74fa5384479df61a234f7ee8bbb873a76 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:32:16 -0300 Subject: [PATCH 19/46] Re-pin the profiling and sizing comments to the rebuilt workload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were left describing the retired block. perf_diff.sh advertised 50.78M cycles, 10,478 keccak and 116 ecsm as the real default, plus ~240 GB monolithic, 158.8 s per recording, ~1.2 GB per bundle and ~52 GB of RAM; the real numbers are 37,137,748 / 6,003 / 164, ~182 GB, ~125 s, ~790 MB and ~45 GiB. Its synthetic figures were stale too, so they were re-measured on the current guest rather than dropped: 7,029,393 cycles, 486 keccak, 80 ecsm for the 20-transfer block, which keeps the point the comment is making — the keccak:ecsm ratio is 37:1 against 6:1, so a hot symbol in one workload need not be hot in the other. bench_abba.sh's sizing table still carried "the pair-delta sd is NOT yet measured" with a 1.0%/2.0% bracket, which the workflow that invokes it no longer says: it now quotes the two measured conditions. A script disagreeing with its own workflow is worse than either being out of date, so the table is the same one, with a note to keep them in step. Also drops perf_diff.sh's "~2% with the clang major" figure. It came from a headline whose own evidence put the effect at 0.13%, and the guest reports the same count here on two toolchains, so the file now points at the README's caveat instead of quoting a number nothing measured. --- scripts/bench_abba.sh | 22 +++++++++++++--------- scripts/perf_diff.sh | 23 +++++++++++++---------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 2966c1c79..7a9f39abf 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -48,17 +48,21 @@ # WORKLOAD=synthetic to reproduce a number recorded against that fixture. # # Sizing at WORKLOAD=real, from the paired t-test (resolvable 95% delta = -# t* x sd / sqrt(N)). The pair-delta sd on the bench runner is NOT yet measured; the -# two columns bracket it between 1.0% (the GPU box's measured 0.64% plus margin) and -# 2.0% (sqrt(2) x the runner's measured 1.43% single-run CV): +# t* x sd / sqrt(N)). The two columns are the same runner under two conditions, not a +# guess bracketing an unknown: its variance is contention, so the single-run CV is +# 0.34% across the proves that got the most CPU and 1.26% across all of a 14-prove +# baseline. sd of a pair delta is sqrt(2) x that. Keep this table in sync with the one +# in .github/workflows/bench-abba.yml: # -# pairs wall resolves (sd 2.0% / sd 1.0%) -# 8 ~50 min 1.7% / 0.8% -# 12 ~72 min 1.3% / 0.6% <- workflow default -# 20 ~1h55m 0.9% / 0.5% -# 32 ~3h 0.7% / 0.4% +# pairs wall resolves (quiet box, sd 0.49% / shared, sd 1.78%) +# 8 ~41 min 0.34% / 1.24% +# 12 ~58 min 0.28% / 1.01% <- workflow default +# 20 ~1h31m 0.21% / 0.78% +# 32 ~2h22m 0.16% / 0.62% # -# Wall assumes epoch 2^22 (158.8 s per prove, two per pair) plus ~8 min of setup. +# Wall assumes epoch 2^22 (~125 s per prove, two per pair) plus ~8 min of setup. Read +# the column the run earned: the exclusivity line printed after the pairs reports the +# CPU share of every prove and flags any under 90% of the batch's best. # The first real ABBA run MEASURES that sd — read it off the `sd` field of the # paired-t line printed below — and this table should be re-pinned to it. # diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index e6e949fb7..ee4a01c37 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -18,17 +18,20 @@ # 22 is the calibrated bench-runner tier, matching /bench; use 23 on a # 128 GiB box (tooling/ethrex-fixtures/README.md, "Choosing the epoch size"). # -# Pick the workload that matches the run you are localizing, because the symbol -# mix follows the block: the real default is 50.78M cycles, 10,478 keccak calls and -# 116 ecsm calls, and the synthetic option (20 plain transfers) inverts that at -# 8.73M cycles, 411 keccak, 80 ecsm — so a hot symbol in one need not be hot in the other. -# 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. +# Pick the workload that matches the run you are localizing, because the symbol mix +# follows the block: the real default is 37,137,748 cycles, 6,003 keccak calls and 164 +# ecsm calls, and the synthetic option (20 plain transfers) shifts that to 7,029,393 +# cycles, 486 keccak, 80 ecsm — a keccak:ecsm ratio of 37:1 against 6:1, so a hot symbol +# in one need not be hot in the other. # -# WORKLOAD=real also switches to a continuation prove (monolithic would need ~240 GB -# at that trace length), which is 158.8 s per recording on the bench runner — five -# recordings, so budget ~13 min of proving, plus ~1.2 GB of disk per bundle and ~52 GB +# Both counts are deterministic for a given ELF and input, and belong to the ethrex rev +# in the manifests (`scripts/set_ethrex_rev.sh --show`) — they move with the rev, with +# guest optimisation, and possibly with the compiler, which the Makefile does not pin. +# See "Pin the ELF whenever you quote a cycle count" in tooling/ethrex-fixtures/README.md. +# +# WORKLOAD=real also switches to a continuation prove (monolithic would need ~182 GB at +# that trace length), which is ~125 s per recording on the bench runner — five +# recordings, so budget ~11 min of proving, plus ~790 MB of disk per bundle and ~45 GiB # of RAM at the default epoch. # # Produces: From dbce739a1ba32a696e66e14de69caba1a16d6fc1 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:32:26 -0300 Subject: [PATCH 20/46] Reuse the baseline prover for the cycle floor instead of building a third one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor check ran before the A/B binaries were built, so it built its own `cli` — at the repo root with default features, sharing neither the worktree's target directory nor its feature set. That is a full extra release build of the prover stack: free on a warm bench runner, a cold build on paid time on the rented GPU box that also runs this script. It now runs after step 2 and executes with `cli_B`, the baseline prover, which measures the workload just as well. Two smaller things in the same block. The execute is now gated on WORKLOAD=real, matching the comparison it feeds — a synthetic run was paying for a measurement nobody read. And the failure path keeps its diagnostic: a bare assignment from a failing command substitution aborts under `set -e` before the message can print, so the operator got a silent death instead of the line telling them to regenerate the fixture. Wrapping it makes the guardrail fail closed *and* say why. --- scripts/bench_abba.sh | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 7a9f39abf..f997c1b8b 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -180,21 +180,13 @@ INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" # `successful_validation == 1` assert in tooling/ethrex-tests), while a small TX_COUNT # legitimately lands near this floor -- a 1-transfer block ran 1.80M cycles before the # bump, an empty one 0.99M. Applying the floor there would reject valid runs. +# +# Runs after step 2 and reuses `cli_B`: the baseline prover executes the workload just as +# well as a freshly built one, and building a third binary here would be a full extra +# release build -- at the repo root with default features, so sharing neither the +# worktree's target dir nor its feature set. Free on a warm runner, a cold build on paid +# time on a rented GPU box. MIN_PLAUSIBLE_CYCLES=1000000 -if [ ! -x ./target/release/cli ]; then - cargo build --release -p cli >/dev/null -fi -workload_cycles="$(./target/release/cli execute "$ELF" --private-input "$INPUT" --cycles \ - | awk '/^Cycles:/ {print $2}')" -if [ "$WORKLOAD" = "real" ] && [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then - echo "ERROR: the workload executed only ${workload_cycles:-0} cycles, below the" >&2 - echo " ${MIN_PLAUSIBLE_CYCLES} floor. The guest almost certainly rejected the input:" >&2 - echo " ELF $ELF" >&2 - echo " input $INPUT" >&2 - echo " Rebuild the fixture at this ethrex rev (make regen-real-block-fixture)." >&2 - exit 1 -fi -echo "==> Workload executes: $workload_cycles cycles" # --- 2. Build (or reuse) both prover binaries --- need_build=0 @@ -245,6 +237,23 @@ else echo " cli_A=${SHA_A:0:10} cli_B=${SHA_B:0:10} features=$BENCH_FEATURES" fi +if [ "$WORKLOAD" = "real" ]; then + # `|| workload_cycles=""` keeps the failure inside a compound: a bare assignment from a + # failing command substitution aborts under `set -e` before the diagnostic below can + # print, which is fail-closed but silent. + workload_cycles="$("$WORK/cli_B" execute "$ELF" --private-input "$INPUT" --cycles 2>&1 \ + | awk '/^Cycles:/ {print $2}')" || workload_cycles="" + if [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then + echo "ERROR: the workload executed only ${workload_cycles:-0} cycles, below the" >&2 + echo " ${MIN_PLAUSIBLE_CYCLES} floor. The guest almost certainly rejected the input:" >&2 + echo " ELF $ELF" >&2 + echo " input $INPUT" >&2 + echo " Rebuild the fixture at this ethrex rev (make regen-real-block-fixture)." >&2 + exit 1 + fi + echo "==> Workload executes: $workload_cycles cycles" +fi + # --- 3. Interleaved A/B/B/A measurement (fresh CSV -- pre-committed batch) --- # Every prove also records the share of CPU it actually got. On a shared box that # is the difference between a number and a coincidence: measured on the bench From 150453bedbf0c1a194b084bbdb2792fbe5aad7d1 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:32:38 -0300 Subject: [PATCH 21/46] Check the shapes before indexing them in the EEST probe The probe read `inputs[0..2]` and `output[33..41]` inside the print, above the asserts that establish those lengths, so a short file or a short output panicked on a slice index instead of failing with the message that names the fixture. The same print had a defensive `output.get(32).unwrap_or(255)` next to a raw `output[33..41]`, which is the inconsistency that hid it. Asserts first, then the print reads plainly. The filename was also byte-sliced to 60 characters, which panics on a multi-byte boundary; it takes 60 chars now. --- .../ethrex-tests/tests/eest_amsterdam_probe.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs index 3bd4f593f..7a9c1c150 100644 --- a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs +++ b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs @@ -33,22 +33,32 @@ fn eest_amsterdam_fixtures_validate_natively() { for path in entries { let inputs = std::fs::read(&path).expect("read fixture"); + let name = path.file_name().unwrap().to_string_lossy(); + // Every field below is read by index, so the shapes are checked first: a short + // file or a short output should fail with the message that says which, not with + // a slice-index panic. `chars()` because a byte slice of a `str` would panic on + // a multi-byte boundary in a non-ASCII filename. + // 2 = the big-endian schema-id prefix. Spelled out rather than imported: the + // constant lives in `ethrex-common`, which this crate does not depend on. + assert!( + inputs.len() >= 2, + "{name}: file is shorter than the schema-id prefix" + ); let schema_id = u16::from_be_bytes([inputs[0], inputs[1]]); let started = std::time::Instant::now(); let output = run_stateless_guest(&inputs, Arc::new(NativeCrypto)); let elapsed = started.elapsed(); - let name = path.file_name().unwrap().to_string_lossy(); + assert_eq!(output.len(), 43, "{name}: unexpected output length"); println!( "{:<60} in={:>9} B schema={:#06x} out={} B ok={} chain_id={} native={:?}", - &name[..name.len().min(60)], + name.chars().take(60).collect::(), inputs.len(), schema_id, output.len(), - output.get(32).copied().unwrap_or(255), + output[32], u64::from_le_bytes(output[33..41].try_into().unwrap()), elapsed, ); - assert_eq!(output.len(), 43, "{name}: unexpected output length"); assert_eq!(output[32], 1, "{name}: native stateless validation failed"); } } From 1cf1b40f05ffd91f0bff0493130f07a8d5fe686a Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:32:38 -0300 Subject: [PATCH 22/46] Drop the converter's unused dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lambda-vm-ethrex-crypto`, `sha2` and `k256` have no remaining users in the crate: the first two were needed by tests that the SSZ rewrite removed, and the `k256` entry existed only to align features with one of them. They are not free — this crate's CI job pays a cold build of the ethrex host tree, C libraries included, to run its two tests. --- tooling/ethrex-block-converter/Cargo.lock | 66 ----------------------- tooling/ethrex-block-converter/Cargo.toml | 3 -- 2 files changed, 69 deletions(-) diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index 057713c5c..e741597cc 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -510,12 +510,6 @@ 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" @@ -777,12 +771,6 @@ dependencies = [ "zeroize", ] -[[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" @@ -855,13 +843,10 @@ dependencies = [ "ethrex-crypto", "ethrex-guest-program", "ethrex-rlp", - "k256", - "lambda-vm-ethrex-crypto", "libssz", "libssz-types", "serde", "serde_json", - "sha2", ] [[package]] @@ -1630,27 +1615,6 @@ 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 = [ - "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" @@ -2342,36 +2306,6 @@ 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" diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 04304554e..7baf86e09 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -46,12 +46,9 @@ serde_json = "1" # 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 -k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic", "expose-field"] } [dev-dependencies] # The LambdaVM `Crypto` implementation used by host-side parity tests. Off # riscv64 its keccak and ECSM paths both fall back to software. -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 ff9d4ab7994c9e03578716aa727a9dbc0ed95864 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 10 Sep 2026 18:32:49 -0300 Subject: [PATCH 23/46] Say plainly that the guest's syscalls patch matches nothing today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment claimed the patched and the pinned copies "happen to coincide" at the current rev, which reads as though the git dependency were in the graph and the override were holding it in place. It is not there at all: the lockfile carries no git+https://github.com/yetanotherco/lambda_vm.git source, because the only thing that pulls those syscalls is `ethrex-guest-program`'s `lambdavm` feature and this manifest does not enable it. Cargo does not warn about the unmatched entry either, which was worth checking before writing it down. The patch stays. The hazard it covers is real and silent — a future ethrex rev that pulls the syscalls from git would have the guest link a different copy than the rest of the repo — and the guest is the one binary where that divergence would matter. --- executor/programs/rust/ethrex/Cargo.toml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 06cae1ae2..8036ae315 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -56,11 +56,12 @@ lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # KZG is not linked, so the point-evaluation precompile (0x0a) is unsupported — # see src/main.rs. ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program", default-features = false } -# `lambda-vm-syscalls` can still reach the graph as a git dep pinned to a commit -# of THIS repo, which upstream bumps on its own schedule. Override it -# with the working-tree copy so the guest always links the same syscalls as the rest -# of the repo — including uncommitted local edits — rather than whatever commit -# ethrex last pinned. (At the current ethrex rev the two happen to coincide; this -# override is what keeps that coincidence from being load-bearing.) +# Defence in depth, inert today. `lambda-vm-syscalls` can reach this graph only as a git +# dep of `ethrex-guest-program`'s `lambdavm` feature, which this manifest does not enable +# — the lockfile carries no `git+https://github.com/yetanotherco/lambda_vm.git` source at +# all, so this patch currently matches nothing and cargo does not warn about it. It stays +# because the hazard it covers is real and silent: a future ethrex rev that pulls those +# syscalls from git would link a different copy than the rest of the repo, and the guest +# is the one binary where that divergence matters. [patch."https://github.com/yetanotherco/lambda_vm.git"] lambda-vm-syscalls = { path = "../../../../syscalls" } From 32024ae74dd1bf94c81dc38eabdfcfb12c733283 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 11 Sep 2026 11:12:02 -0300 Subject: [PATCH 24/46] Keep the EEST probe out of the tree until its workload lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in the repository produces `eest_amsterdam_bins` — no make target, no workflow, no ignore entry — so both tests took their early return and passed on every checkout: dead test code by construction. The inputs are lifted by hand out of an execution-specs release tarball with a script that is not in the repository either. It comes back together with the EEST workload, and with the step that produces its inputs, so that the tests it adds actually run. Its two tests need splitting first: the positive scan took every `.bin` in the directory and required each to validate, including the legacy rkyv fixture that the second test requires to be REJECTED, so supplying that fixture would have made the first test fail. --- .../tests/eest_amsterdam_probe.rs | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 tooling/ethrex-tests/tests/eest_amsterdam_probe.rs diff --git a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs b/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs deleted file mode 100644 index 7a9c1c150..000000000 --- a/tooling/ethrex-tests/tests/eest_amsterdam_probe.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Probe: do the upstream EEST Amsterdam benchmark fixtures execute on the -//! guest this rev pins? Committed as the measuring tool for adding an EEST -//! workload alongside the real-mix one; its inputs are not committed, so on a -//! plain checkout it skips (see below) and costs CI nothing. -//! -//! Inputs are `statelessInputBytes` lifted verbatim out of -//! `tests-zkevm-benchmark@v0.8.2` blockchain tests, so nothing is rebuilt host -//! side. Run with: -//! cargo test --manifest-path tooling/ethrex-tests/Cargo.toml --release \ -//! --test eest_amsterdam_probe -- --nocapture - -use ethrex_guest_program::crypto::NativeCrypto; -use ethrex_guest_program::l1::run_stateless_guest; -use std::sync::Arc; - -const BIN_DIR: &str = "../../eest_amsterdam_bins"; - -#[test] -fn eest_amsterdam_fixtures_validate_natively() { - // The .bin files are downloaded/generated artifacts, not committed: a checkout - // without them skips rather than fails, so this probe can live in the tree. - if !std::path::Path::new(BIN_DIR).is_dir() { - println!("{BIN_DIR} absent — skipping"); - return; - } - let mut entries: Vec<_> = std::fs::read_dir(BIN_DIR) - .expect("bin dir") - .filter_map(|e| e.ok().map(|e| e.path())) - .filter(|p| p.extension().is_some_and(|x| x == "bin")) - .collect(); - entries.sort(); - assert!(!entries.is_empty(), "no .bin found in {BIN_DIR}"); - - for path in entries { - let inputs = std::fs::read(&path).expect("read fixture"); - let name = path.file_name().unwrap().to_string_lossy(); - // Every field below is read by index, so the shapes are checked first: a short - // file or a short output should fail with the message that says which, not with - // a slice-index panic. `chars()` because a byte slice of a `str` would panic on - // a multi-byte boundary in a non-ASCII filename. - // 2 = the big-endian schema-id prefix. Spelled out rather than imported: the - // constant lives in `ethrex-common`, which this crate does not depend on. - assert!( - inputs.len() >= 2, - "{name}: file is shorter than the schema-id prefix" - ); - let schema_id = u16::from_be_bytes([inputs[0], inputs[1]]); - let started = std::time::Instant::now(); - let output = run_stateless_guest(&inputs, Arc::new(NativeCrypto)); - let elapsed = started.elapsed(); - assert_eq!(output.len(), 43, "{name}: unexpected output length"); - println!( - "{:<60} in={:>9} B schema={:#06x} out={} B ok={} chain_id={} native={:?}", - name.chars().take(60).collect::(), - inputs.len(), - schema_id, - output.len(), - output[32], - u64::from_le_bytes(output[33..41].try_into().unwrap()), - elapsed, - ); - assert_eq!(output[32], 1, "{name}: native stateless validation failed"); - } -} - -/// What the retired real-block fixture does on this branch's guest: the rkyv -/// archive's first two bytes are not the Amsterdam schema id, so the input is -/// rejected and the guest commits an all-zero result. It does NOT error out — -/// which is exactly why a stale fixture would silently benchmark nothing. -/// -/// Reads a copy kept outside `executor/tests`, since that directory now holds the -/// regenerated (valid) fixture for the same block. Skips when the copy is absent. -#[test] -fn legacy_rkyv_real_block_is_rejected_silently() { - let path = format!("{BIN_DIR}/legacy_rkyv_real_block.bin"); - if !std::path::Path::new(&path).is_file() { - println!("{path} absent — skipping"); - return; - } - let inputs = std::fs::read(&path).expect("legacy fixture"); - let output = run_stateless_guest(&inputs, Arc::new(NativeCrypto)); - println!( - "legacy fixture: {} B, first two bytes {:#04x}{:02x}, output {} B, ok={}, all_zero={}", - inputs.len(), - inputs[0], - inputs[1], - output.len(), - output[32], - output.iter().all(|b| *b == 0), - ); - assert_eq!(output.len(), 43); - assert_eq!(output[32], 0, "legacy input must not validate"); -} From a5d4c740e59e054b1b52ee24923a6a886bba6dad Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 11 Sep 2026 11:12:10 -0300 Subject: [PATCH 25/46] Say what the converter's tests actually cover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSZ rewrite deleted the two tests that exercised conversion and left two that only assert a rejection, but three comments still described the old ones: the Makefile target promised "host-side parity through the guest's own Crypto impl ... and the reproducibility digest", the rev-stamp comment warned about a `conversion_is_reproducible` failure that can no longer happen, and the crate's own comment claimed the tests exercise the conversion itself. What can be reached today is the two rejection paths; the success path needs a cache from a network running Amsterdam, carrying `slot_number` and `block_access_list_hash`, and none is published. The comments say that now, including that the encoder is exercised by hand and by nothing automatic. Two leftovers in the same crate: the README still described the benchmark fixture as a fetched release artifact with an SSZ artifact to publish, which this branch replaced with a generator, and dropping the unused dependencies left their justifications behind — eleven lines explaining a `k256` entry that is gone, and an empty `[dev-dependencies]` holding only comments about two deleted ones. --- Makefile | 29 +++++++++++--------- tooling/ethrex-block-converter/Cargo.toml | 18 ------------- tooling/ethrex-block-converter/README.md | 32 +++++++++++------------ 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/Makefile b/Makefile index 2dfc12277..5dc403512 100644 --- a/Makefile +++ b/Makefile @@ -471,23 +471,27 @@ print-real-block-cache-url: # path builds this crate. # # 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 serialized SSZ 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. +# benchmarks currently prove, and stays there across a repoint. 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. +# +# What those tests reach is only the REJECTION paths: a pre-Amsterdam cache, and an +# unmappable network. The conversion's success path cannot be covered yet — it needs a +# cache carrying `slot_number` and `block_access_list_hash`, and no published cache has +# them until a network runs Amsterdam. Until one does, this crate's SSZ encoder is +# exercised by hand (`cargo run`) and by nothing automatic. # # 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. +# the test input move under a fixed set of assertions. ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383 ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656 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. +# rev-stamped marker makes a re-pin discard the stale cache. Nothing downstream would +# catch it otherwise: the two tests that remain assert that a cache is REJECTED, and a +# stale cache satisfies that just as well as the right one. ETHREX_REPLAY_REV_STAMP := tooling/ethrex-block-converter/caches/.replay-rev-$(ETHREX_REPLAY_REV) $(ETHREX_REPLAY_REV_STAMP): @@ -503,9 +507,10 @@ $(ETHREX_CONVERTER_CACHE): $(ETHREX_REPLAY_REV_STAMP) 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-block-converter.yml), not on every PR. +# Converter correctness, as far as it is testable today: a pre-Amsterdam cache and an +# unmappable network are both rejected. The success path needs an Amsterdam cache — +# see the comment above. Runs on changes to the converter +# (see .github/workflows/ethrex-block-converter.yml), not on every PR. test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE) cd tooling/ethrex-block-converter && cargo test --locked --release diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 7baf86e09..877a3087f 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -34,21 +34,3 @@ libssz = "=0.3.0" libssz-types = "=0.3.0" serde = { version = "1", features = ["derive"] } serde_json = "1" - -# 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. This crate is built both ways against -# one target dir: `cargo run` for `make regen-real-block-fixture`, `cargo test` for -# `make test-ethrex-real-block-converter`. Without this line the two resolve -# different k256 feature sets, so alternating between them recompiles k256 and every -# ethrex crate above it. 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 - -[dev-dependencies] -# The LambdaVM `Crypto` implementation used by host-side parity tests. Off -# riscv64 its keccak and ECSM paths both fall back to software. -# 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. diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 2f6d766a2..5f3197ea7 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -38,25 +38,25 @@ coupled to the ethrex revision. ## Real-block fixture -The benchmark fixture is a fetched release artifact, not a build dependency: - -```bash -make ethrex-real-block-fixture -``` - -After an ethrex revision bump, regenerate it from the matching cache: +The benchmark fixture is generated rather than fetched, and not by this crate: +`tooling/ethrex-fixtures --bin real_block` rebuilds a real mainnet block as an +Amsterdam block. What is hosted is that block's replay cache, which is +fork-independent. `make ethrex-real-block-fixture` produces the fixture when it is +missing; after an ethrex revision bump, rebuild it and re-baseline its digest: ```bash make regen-real-block-fixture -sha256sum "$(make -s print-real-block-fixture)" +sha256sum "$(make -s print-real-block-fixture)" # -> ETHREX_REAL_BLOCK_FIXTURE_SHA256 ``` -Publish the resulting SSZ artifact and matching Amsterdam cache, then update -their URLs and checksums in the Makefile before enabling the real-block -benchmark. The old release assets are rkyv `ProgramInput` artifacts and are -incompatible with the pinned ethrex. +There is no artifact left to publish: the generator is deterministic, so re-running +it is how the fixture is replaced. The old release assets are rkyv `ProgramInput` +artifacts and are incompatible with the pinned ethrex. + +## What the tests cover -The converter's tests use the Hoodi cache under `caches/` and verify that an -unmapped network is rejected. The checked release cache predates Amsterdam, so -it is intentionally rejected until a cache containing the new payload fields -is published. +Both tests assert a rejection. The pinned Hoodi cache predates Amsterdam and is +refused rather than rewritten, and an unmappable network is refused too. The +success path — cache in, valid SSZ out — has no automated coverage: it needs a +cache from a network that runs Amsterdam, and none is published yet. Until one is, +the encoder is exercised by hand through the `cargo run` above. From acacd85bf73bb71dc5b56d0c65c6a49c65c3c5bd Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 11 Sep 2026 11:12:10 -0300 Subject: [PATCH 26/46] Re-pin the last epoch-sizing number in perf_diff.sh The comment on `EPOCH_SIZE_LOG2` still sized the epoch against the retired workload's ~52 GB peak, while the header of the same file already carried the rebuilt one. It is 44.81 GiB at 2^22 on that runner and 2^23 crosses 50 GiB, which is what benchmark-pr.yml and bench-abba.yml already say. --- scripts/perf_diff.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index ee4a01c37..8e3150761 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -52,8 +52,9 @@ REF_A="$1" REF_B="${2:-origin/main}" WORKLOAD="${WORKLOAD:-real}" # 2^22: the calibrated tier for the bench server this script targets, same as -# /bench's real-block arm. Memory picks it, not speed — that server peaks at ~52 GB on -# a >=64 GiB floor, and 2^23 measured 60 GiB on a roomier box, so it would not fit here. +# /bench's real-block arm. Memory picks it, not speed — this workload peaks at 44.81 GiB +# there against a >=64 GiB floor, and 2^23 would take it past 50 GiB for ~7% of wall +# (the epoch sweep is in tooling/ethrex-fixtures/README.md). EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-22}" case "$WORKLOAD" in synthetic|real) ;; From 1ca1030bc9420c6aeb55c390f8000d45dd1c3bf1 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 11 Sep 2026 11:12:10 -0300 Subject: [PATCH 27/46] Drop the copy note that the shared module made false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build_stateless_input` and its helpers now live in `src/lib.rs` and this binary imports them, so the note saying they are copied from `main.rs` — and that lifting them into a module was something "a real PR" should do — describes the tree before that change. --- tooling/ethrex-fixtures/src/bin/real_block.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index 5f382cdb8..081a70000 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -17,10 +17,6 @@ //! //! Usage: //! cargo run --release --bin real_block -- -//! -//! NOTE: `build_stateless_input` and its helpers are copied from `main.rs` -//! rather than shared. A real PR should lift them into a module; keeping this -//! binary self-contained leaves the committed generator untouched. use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; From 82a70b76198576cbd862590cba5a7ec0631c2675 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 10:29:48 -0300 Subject: [PATCH 28/46] Say that the real-block fixture is built, not fetched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This branch replaced the hosted fixture with a generator, and four comments across three scripts still described the old path — two of them stating the opposite of what happens now. `bench_verify.sh` called it "Fetched by URL + sha256, not built: no converter and no ethrex host dependency tree on this path", and `perf_diff.sh` "a URL + sha256 download, not a build". On a digest miss both now run a cargo build of `tooling/ethrex-fixtures`. What is still fetched by URL and sha256 is the block's replay cache, which the fixture is generated from; the fixture is 549 KB, not the ~1 MB the old artifact was. Also the GPU workflow's "no offer" error, which still reported a 48 GB RAM floor after this branch raised the query to 64. --- .github/workflows/benchmark-gpu.yml | 2 +- scripts/bench_abba.sh | 2 +- scripts/bench_verify.sh | 15 ++++++++------- scripts/perf_diff.sh | 11 ++++++----- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 1f715d45a..c31cc7e2f 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -276,7 +276,7 @@ jobs: sleep "$OFFER_INTERVAL" done if [ -z "$OFFER_ID" ]; then - echo "::error::No RTX 5090 offer matched after $OFFER_ATTEMPTS attempts (whole-machine gpu_frac=1, 16-32 cores, >=48GB RAM, >=64GB disk, driver>=${MIN_DRIVER}, reliability>=0.95, cuda_max_good>=12.8, <= \$${PRICE_CAP}/hr). Full query echoed above." + echo "::error::No RTX 5090 offer matched after $OFFER_ATTEMPTS attempts (whole-machine gpu_frac=1, 16-32 cores, >=64GB RAM, >=64GB disk, driver>=${MIN_DRIVER}, reliability>=0.95, cuda_max_good>=12.8, <= \$${PRICE_CAP}/hr). Full query echoed above." exit 1 fi echo "id=$OFFER_ID" >> "$GITHUB_OUTPUT" diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index f997c1b8b..fc0564c2a 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -38,7 +38,7 @@ # large continuation trace where GPU-residency wins are visible). # Ignored when WORKLOAD=real. # WORKLOAD=real|synthetic (default real) which block to prove. `real` -# fetches the real-block fixture (block identity lives in the Makefile) and +# builds the real-block fixture (block identity lives in the Makefile) and # forces --continuations; TX_COUNT and CONTINUATIONS do not apply to it. # # On WORKLOAD: a real block is keccak- and trie-bound, while the synthetic option is diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 9deaea0df..5901f4bae 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -31,7 +31,7 @@ # n=4 it is 0.125 and the arm can only ever report BORDERLINE, however large and # clean the effect. # WORKLOAD=real|synthetic (default real) which BLOCK both arms prove. -# `real` fetches the real-block fixture (identity lives in the Makefile) and runs +# `real` builds 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 is the @@ -181,12 +181,13 @@ if [ ! -f "$ELF_REL" ]; then make "$ELF_REL" fi if [ "$WORKLOAD" = "real" ]; then - # ~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. Unconditional on - # purpose: the target hashes whatever is already on disk on every invocation, which - # is how a stale copy left by an earlier block or an interrupted fetch gets caught. - # A match costs ~35 ms, so there is nothing to gate it on. - echo "==> Verifying ethrex real-block fixture (fetches on a digest miss)" + # 549 KB, gitignored, never in a fresh checkout. GENERATED from the block's replay + # cache (the cache is what gets fetched, by URL + sha256), so a miss here is a cargo + # build of tooling/ethrex-fixtures and not a download. Unconditional on purpose: the + # target hashes whatever is already on disk on every invocation, which is how a stale + # copy left by an earlier block or an interrupted write gets caught. A match costs + # ~35 ms, so there is nothing to gate it on. + echo "==> Verifying ethrex real-block fixture (regenerates on a digest miss)" make ethrex-real-block-fixture elif [ ! -f "$INPUT_REL" ]; then echo "==> Generating ethrex 20-transfer fixture (missing)" diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index 8e3150761..aba7f990b 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -82,11 +82,12 @@ 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; } if [ "$WORKLOAD" = "real" ]; then - # ~1 MB, gitignored, never in a fresh checkout; fetch rather than abort. This is a - # URL + sha256 download, not a build. Unconditional on purpose: the target hashes - # whatever is on disk on every invocation, which is how a stale copy gets caught. - # A match costs ~35 ms. - echo "==> Verifying ethrex real-block fixture (fetches on a digest miss)" + # 549 KB, gitignored, never in a fresh checkout; build rather than abort. What is + # fetched by URL + sha256 is the block's replay cache; the fixture itself is generated + # from it, so a miss here costs a cargo build of tooling/ethrex-fixtures. Unconditional + # on purpose: the target hashes whatever is on disk on every invocation, which is how a + # stale copy gets caught. A match costs ~35 ms. + echo "==> Verifying ethrex real-block fixture (regenerates on a digest miss)" make ethrex-real-block-fixture elif [ ! -f "$INPUT_REL" ]; then echo "ERROR: missing $INPUT_REL — run WORKLOAD=synthetic scripts/bench_abba.sh once (it generates the synthetic fixture; the default workload is real and would not)." >&2 From 2a9e406b514098f4cc9302b935a6bb85c92b16f2 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 10:31:15 -0300 Subject: [PATCH 29/46] Build the real-block fixture under the prover builds, not before them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a persistent runner the fixture is already on disk and `make ethrex-real-block-fixture` is a 35 ms digest check. The GPU workflow runs this script on a freshly rented, hourly-billed box where the file is absent, so the target falls through to the generator — a cold cargo build of the ethrex host tree, blst and c-kzg included, at CARGO_BUILD_JOBS=8 — and it ran before the two prover builds, on a box that is otherwise idle. Backgrounding it puts it under them: separate workspace and target dir, so they compile in parallel. Nothing between there and the cycle floor reads the fixture, which is where it is waited on; a failure still stops the run and prints the build log, and both traps kill the job so a failed prover build cannot leave cargo running on the shared bench runner. The GPU ETA carried the same assumption — its 12-minute intercept was measured when the fixture was a download — so it is 18 now, for the part of the fixture build that outlasts the cuda build. Neither has been measured. The CPU sizing tables are untouched: that runner is persistent, so the fixture is already there. --- .github/workflows/benchmark-gpu.yml | 10 +++++++++- scripts/bench_abba.sh | 31 ++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index c31cc7e2f..3e867831c 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -176,7 +176,15 @@ jobs: // Per-prove wall varies with the rented host's CPU // (the prover is partly host-CPU-bound), so the slope is the measured one // and the intercept carries slack for a colder box. - const mins = 12 + Number(process.env.PAIRS) * 4; + // The intercept also has to cover the real-block fixture, which this box + // BUILDS rather than downloads -- it is generated from the block's replay + // cache, and a rented box is always a fresh checkout -- i.e. a cold cargo + // build of tooling/ethrex-fixtures, ethrex host tree and C dependencies + // included, at CARGO_BUILD_JOBS=8. bench_abba.sh backgrounds it so it runs + // under the dual cuda build rather than before it, so what reaches the ETA is + // only the part that outlasts that build. Neither has been measured, which is + // why this is 18 and not a number with a decimal point. + const mins = 18 + Number(process.env.PAIRS) * 4; 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, diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index fc0564c2a..65a207c37 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -156,8 +156,21 @@ if [ "$WORKLOAD" = "real" ]; then # checkout. Built from the block's replay cache (fetched by URL + sha256): ethrex 25's # guest decodes only the Amsterdam schema, so no hosted artifact for this block can be # valid. The generator validates the block through the guest before writing. - echo "==> Building ethrex real-block fixture (from its replay cache)" - make ethrex-real-block-fixture + # + # Backgrounded so it runs UNDER step 2's prover builds instead of before them. On a + # persistent runner the fixture is already on disk and this is a ~35 ms digest check; + # on a freshly rented, hourly-billed box it is a cold cargo build of the ethrex host + # tree, and that is minutes the two builds would otherwise pay one after the other. + # Separate workspace and target dir, so they compile in parallel -- what they do share + # is cargo's package-cache lock, which serialises downloads for a few seconds and + # nothing else. Nothing before the cycle floor reads the fixture, which is where this + # is waited on. + echo "==> Building ethrex real-block fixture (from its replay cache; in background)" + make ethrex-real-block-fixture > "$WORK/fixture_build.log" 2>&1 & + FIXTURE_PID=$! + # A failure below must not leave a cargo build running: on the shared bench runner the + # next batch would measure against it. + trap 'kill "${FIXTURE_PID:-}" 2>/dev/null || true' EXIT elif [ ! -f "$INPUT_REL" ]; then echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" ( cd tooling/ethrex-fixtures && cargo build --release ) @@ -201,7 +214,12 @@ elif [ "$(cat "$WORK/cli_A.sha" 2>/dev/null)" != "$SHA_A $BENCH_FEATURES" ] || \ need_build=1 fi if [ "$need_build" = "1" ]; then - cleanup() { git worktree remove --force "$WT" 2>/dev/null || true; } + # Takes the backgrounded fixture build with it: `trap cleanup EXIT` replaces the trap + # set for it in step 1, so this is what stops it when a build here fails. + cleanup() { + git worktree remove --force "$WT" 2>/dev/null || true + kill "${FIXTURE_PID:-}" 2>/dev/null || true + } trap cleanup EXIT git worktree remove --force "$WT" 2>/dev/null || true echo "==> Building both prover binaries in isolated worktree $WT" @@ -238,6 +256,13 @@ else fi if [ "$WORKLOAD" = "real" ]; then + if ! wait "$FIXTURE_PID"; then + echo "ERROR: the real-block fixture could not be built:" >&2 + cat "$WORK/fixture_build.log" >&2 + exit 1 + fi + trap - EXIT + echo "==> Fixture ready: $INPUT_REL" # `|| workload_cycles=""` keeps the failure inside a compound: a bare assignment from a # failing command substitution aborts under `set -e` before the diagnostic below can # print, which is fail-closed but silent. From 76e9b682118d40efda1fdacef03e75a96e7ad664 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:11:08 -0300 Subject: [PATCH 30/46] Keep the fixture build alive through the successful prover builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cleanup` is not only the EXIT trap: it is called explicitly once both prover binaries are built, so folding the fixture job's `kill` into it SIGTERM'd the background build the moment the cargo builds finished, and the `wait` below then read 143 and aborted the run with an empty log. It survived only when the fixture happened to finish first — which on a freshly rented box, where that build is the long one, it does not. `cleanup` now removes the worktree and nothing else, and the kill lives in `kill_fixture`, named once and installed as the EXIT trap in all three states: while the fixture builds, while the provers build (`cleanup; kill_fixture`), and again after the successful build, where `trap - EXIT` used to leave the window between there and the wait uncovered. --- scripts/bench_abba.sh | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 65a207c37..0dd5b69b4 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -146,6 +146,8 @@ fi mkdir -p "$WORK" # --- 1. Guest ELF + fixture (identical for both sides; build once if missing) --- +# Unset PID (WORKLOAD=synthetic) and already-reaped PID both fall through to `|| true`. +kill_fixture() { kill "${FIXTURE_PID:-}" 2>/dev/null || true; } if [ ! -f "$ELF_REL" ]; then echo "==> Building ethrex guest ELF (missing)" export SYSROOT_DIR="${SYSROOT_DIR:-$HOME/.lambda-vm-sysroot}" @@ -169,8 +171,8 @@ if [ "$WORKLOAD" = "real" ]; then make ethrex-real-block-fixture > "$WORK/fixture_build.log" 2>&1 & FIXTURE_PID=$! # A failure below must not leave a cargo build running: on the shared bench runner the - # next batch would measure against it. - trap 'kill "${FIXTURE_PID:-}" 2>/dev/null || true' EXIT + # next batch would measure against it. No-op once the wait below has reaped it. + trap kill_fixture EXIT elif [ ! -f "$INPUT_REL" ]; then echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" ( cd tooling/ethrex-fixtures && cargo build --release ) @@ -214,13 +216,12 @@ elif [ "$(cat "$WORK/cli_A.sha" 2>/dev/null)" != "$SHA_A $BENCH_FEATURES" ] || \ need_build=1 fi if [ "$need_build" = "1" ]; then - # Takes the backgrounded fixture build with it: `trap cleanup EXIT` replaces the trap - # set for it in step 1, so this is what stops it when a build here fails. - cleanup() { - git worktree remove --force "$WT" 2>/dev/null || true - kill "${FIXTURE_PID:-}" 2>/dev/null || true - } - trap cleanup EXIT + # `cleanup` removes the worktree and NOTHING else, because it is also called on the + # success path below, where the fixture build must survive. Killing it belongs to the + # EXIT trap only -- which has to name both, since `trap ... EXIT` here replaces the one + # step 1 set for the fixture. + cleanup() { git worktree remove --force "$WT" 2>/dev/null || true; } + trap 'cleanup; kill_fixture' EXIT git worktree remove --force "$WT" 2>/dev/null || true echo "==> Building both prover binaries in isolated worktree $WT" git worktree add --detach "$WT" "$SHA_B" >/dev/null @@ -249,7 +250,9 @@ if [ "$need_build" = "1" ]; then build_cli "$SHA_B" cli_B build_cli "$SHA_A" cli_A cleanup - trap - EXIT + # Back to step 1's trap, not `trap - EXIT`: the fixture build is still running and an + # abort between here and the wait below should still take it down. + trap kill_fixture EXIT else echo "==> Reusing cached binaries (refs + features match; REBUILD=1 to force):" echo " cli_A=${SHA_A:0:10} cli_B=${SHA_B:0:10} features=$BENCH_FEATURES" From fc2dfe8fe0be2b58cb56ae7331f98ebfb895d20b Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:11:18 -0300 Subject: [PATCH 31/46] Fail the converter's rejection test when its cache is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `caches/` is gitignored and fetched on demand, and the test accepted a plain `No such file or directory` as if it were the Amsterdam rejection, so on a clean checkout it went green having asserted nothing — and `contains("os error")` was broad enough to swallow a permissions failure or a truncated download too. It asserts the cache is there first, with the same message `unmappable_network_is_rejected` already uses, and then asserts the rejection is the Amsterdam one. --- tooling/ethrex-block-converter/src/main.rs | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index 7f5bdfd0f..afca64fb4 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -297,22 +297,18 @@ mod tests { #[test] fn legacy_cache_is_rejected_without_amsterdam_fields() { - let result = stateless_input_from_cache(CACHE); - let Err(error) = result else { + // Fail on the missing cache instead of accepting its I/O error as the rejection: + // every error reads as a pass otherwise, and this test would go green on a clean + // checkout having asserted nothing. `unmappable_network_is_rejected` reports the + // same condition the same way. + assert!(std::path::Path::new(CACHE).exists(), "{CACHE_MISSING}"); + let Err(error) = stateless_input_from_cache(CACHE) else { panic!("the checked-in replay cache unexpectedly has Amsterdam fields"); }; - if !std::path::Path::new(CACHE).exists() { - assert!( - error.to_string().contains("No such file") - || error.to_string().contains("os error"), - "{CACHE_MISSING}: {error}" - ); - } else { - assert!( - error.to_string().contains("Amsterdam"), - "unexpected error: {error}" - ); - } + assert!( + error.to_string().contains("Amsterdam"), + "unexpected error: {error}" + ); } #[test] From a2ad4580f0028a537c18acd49f1fd582b2d1288e Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:44:09 -0300 Subject: [PATCH 32/46] Describe the real-block artifacts as one fetched and one generated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Makefile's header for the workload still introduced both artifacts as "FETCHED rather than built", and justified it with a paragraph about keeping the ethrex host dependency tree off the path of everyone who just wants to run a benchmark — which is exactly what a checkout without the fixture now pays, and what the backgrounding in bench_abba.sh exists to hide. It now separates the two: the cache is fetched because it is fork-independent, the fixture is generated from it because the schema id is a fork declaration and a hosted one would have to be rebuilt and published by hand on every rev bump. The cost of that choice is named rather than implied. `benchmark-pr.yml` said the same thing about the fixture while guarding on the CACHE url, which is what it actually needs. And the line saying the converter is the regeneration tool for rev bumps is gone: `ethrex-fixtures --bin real_block` is. --- .github/workflows/benchmark-pr.yml | 7 +++--- Makefile | 36 ++++++++++++++++++------------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index b96e938f1..fd0b3ee04 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -248,9 +248,10 @@ jobs: # 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 - # artifact is uploaded: warn instead of failing, so a push to main cannot go + # What is fetched from a pinned URL + sha256 is the block's replay CACHE; the + # fixture is generated from it (see the Makefile), which is why the guard below + # asks for the cache URL. It covers the window after a repoint but before the + # new cache is uploaded: warn instead of failing, so a push to main cannot go # red over an upload nobody in CI can perform. On push and workflow_dispatch # the growth sweep still runs and feeds the baseline artifact (no comment is # posted on those events — the Comment step is gated to comment triggers); diff --git a/Makefile b/Makefile index 5dc403512..ad68883e3 100644 --- a/Makefile +++ b/Makefile @@ -277,24 +277,32 @@ test-rust: compile-programs-rust # ===== Real block: the benchmark workload ===== # # 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: +# from tooling/ethrex-fixtures. Two artifacts, both gitignored, and only one of +# them is fetched: # -# the fixture the schema-prefixed SSZ input the benchmarks prove -# the cache the ethrex-replay JSON it was converted from (~2 MB), read only -# by `regen-real-block-fixture`. The converter's TESTS read a -# different, upstream-pinned cache — see below. +# the cache the ethrex-replay JSON (~2 MB), FETCHED by URL + sha256. It is +# fork-independent, which is why it survives a rev bump and the +# fixture does not. The converter's TESTS read a different, +# upstream-pinned cache — see below. +# the fixture the schema-prefixed SSZ input the benchmarks prove, GENERATED +# from that cache by `tooling/ethrex-fixtures --bin real_block` +# and pinned by sha256 like a download would be. # -# 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. +# Generating it is what makes a rev bump a re-run rather than an upload: the schema +# id is the fork declaration, so a hosted fixture would have to be rebuilt and +# published by hand on every bump, and a stale one benchmarks nothing (496 cycles, +# exit 0). The cost is that a checkout without the fixture pays a cold build of the +# ethrex host tree to produce it — real on a rented GPU box, ~35 ms on a persistent +# runner where the digest already matches. scripts/bench_abba.sh backgrounds that +# build so it runs under the prover builds. +# +# Fetching the cache rather than an RPC replay 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 replay-locally route (~4 minutes and ~700 +# calls against an archive RPC) and trivial by this one. # # 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. +# below. It is no longer what regenerates this fixture. # # ---- Repointing to a different block ---- # These SIX lines and nothing else. Every path below derives from them, the From 37d228c23d8ca315222250d82bb4b531f71c78ae Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:44:09 -0300 Subject: [PATCH 33/46] Stop pointing the fixture README at a file that is not in the repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` exists in neither this branch nor `main` — it is a local report — so the sentence sent the reader to nothing. It now says what the EEST fixtures are and where they come from, which is the part that was worth saying. --- tooling/ethrex-fixtures/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index d05e18dca..057955862 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -89,8 +89,9 @@ transactions dearer. So what this fixture is, is a *real-mix* Amsterdam block — real contract code, real calldata, real signatures, real trie depth — and not a replay of mainnet economics. For workloads whose gas limits were computed for Amsterdam, use the -EEST benchmark fixtures; `ETHREX_BENCH_WORKLOAD_AFTER_BUMP.md` in the repository -root has both sets measured side by side. +EEST benchmark fixtures upstream publishes with `tests-zkevm-benchmark`: they carry +`statelessInputBytes` the guest reads as-is, one dimension stressed per fixture, +which is the part a real block does not give. Measured on the guest ELF at ethrex `8effcb06`: **37,137,748 cycles**, 6,003 keccak calls, 164 ECSM calls. Fixture: 549,144 bytes. From 8b24d3d4e561f4a8f6a9e3781f6879055f22635c Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 15:53:00 -0300 Subject: [PATCH 34/46] Move the SSZ encoder into a crate all three producers share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ethrex-fixtures/src/lib.rs` was added in this branch to end the copy between the two generators, and the converter kept a third copy of the same ~90 lines — the one whose success path has no automated coverage, so a divergence on the next rev bump would surface only when somebody ran it by hand. The encoder is now `tooling/ethrex-ssz-input`, a lib-only crate both depend on by path; what stays in each caller is where the block comes from, which is what actually differs. The shared crate carries nothing extra: `ethrex-common`, `ethrex-guest-program`, `ethrex-rlp` and the two libssz pins, all of which both consumers already had. Having the converter depend on `ethrex-fixtures` directly would instead have pulled in `ethrex-l2-rpc` — the one dependency of the generators its graph does not already carry. It also absorbs the output layout, which was written out as `output.len() != 43 || output[32] == 0` in three files with nothing naming either number. `validate_natively` does that check, `GUEST_OUTPUT_LEN` and `VALIDATION_FLAG` name the fields, and each call site keeps its own message. `tooling/ethrex-tests` keeps its two asserts: they distinguish a wrong length from a rejected block, which a single call would flatten. Verified rather than assumed: the fixture regenerates byte-for-byte identical (sha256 `08a52e10…`, the digest the Makefile pins), the converter's two tests pass against the real Hoodi cache, and both crates build under `--locked`, which is how CI builds them. `scripts/set_ethrex_rev.sh` and the path filters of both workflows now name the new manifest, so a rev bump re-pins it and a change to it still triggers the jobs that build it. --- .github/workflows/benchmark-pr.yml | 1 + .github/workflows/ethrex-block-converter.yml | 2 + scripts/set_ethrex_rev.sh | 1 + tooling/ethrex-block-converter/Cargo.lock | 13 ++ tooling/ethrex-block-converter/Cargo.toml | 3 + tooling/ethrex-block-converter/src/main.rs | 167 ++---------------- tooling/ethrex-fixtures/Cargo.lock | 12 ++ tooling/ethrex-fixtures/Cargo.toml | 2 + tooling/ethrex-fixtures/src/bin/real_block.rs | 9 +- tooling/ethrex-fixtures/src/main.rs | 10 +- tooling/ethrex-ssz-input/Cargo.toml | 23 +++ .../src/lib.rs | 40 ++++- 12 files changed, 112 insertions(+), 171 deletions(-) create mode 100644 tooling/ethrex-ssz-input/Cargo.toml rename tooling/{ethrex-fixtures => ethrex-ssz-input}/src/lib.rs (76%) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index fd0b3ee04..829bc66f9 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -12,6 +12,7 @@ on: - 'executor/**' - 'bin/cli/**' - 'tooling/ethrex-fixtures/**' + - 'tooling/ethrex-ssz-input/**' # syscalls is linked into the guest ELF this job builds, so a change confined to # it changes the bytes proven — a guest allocator swap moves cycles on every # workload. Without it main's baseline would stay stale until some prover file diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index 8a97a335d..899a3ea01 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -17,6 +17,7 @@ on: paths: - 'tooling/ethrex-fixtures/**' - 'tooling/ethrex-block-converter/**' + - 'tooling/ethrex-ssz-input/**' - 'tooling/ethrex-tests/**' - 'Makefile' - '.github/workflows/ethrex-block-converter.yml' @@ -25,6 +26,7 @@ on: paths: - 'tooling/ethrex-fixtures/**' - 'tooling/ethrex-block-converter/**' + - 'tooling/ethrex-ssz-input/**' - 'tooling/ethrex-tests/**' - 'Makefile' diff --git a/scripts/set_ethrex_rev.sh b/scripts/set_ethrex_rev.sh index 5964420e7..fe6520b4b 100755 --- a/scripts/set_ethrex_rev.sh +++ b/scripts/set_ethrex_rev.sh @@ -40,6 +40,7 @@ TOOLING_MANIFESTS=( tooling/ethrex-tests/Cargo.toml tooling/ethrex-block-converter/Cargo.toml tooling/ethrex-fixtures/Cargo.toml + tooling/ethrex-ssz-input/Cargo.toml ) MANIFESTS=("${GUEST_MANIFESTS[@]}" "${TOOLING_MANIFESTS[@]}") diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index e741597cc..c243d9778 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -843,6 +843,7 @@ dependencies = [ "ethrex-crypto", "ethrex-guest-program", "ethrex-rlp", + "ethrex-ssz-input", "libssz", "libssz-types", "serde", @@ -897,6 +898,7 @@ dependencies = [ "libssz-types", "lru", "once_cell", + "rayon", "rkyv", "rustc-hash", "secp256k1", @@ -1064,6 +1066,17 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "ethrex-ssz-input" +version = "0.1.0" +dependencies = [ + "ethrex-common", + "ethrex-guest-program", + "ethrex-rlp", + "libssz", + "libssz-types", +] + [[package]] name = "ethrex-storage" version = "26.0.0" diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 877a3087f..66de00d6d 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -7,6 +7,9 @@ edition = "2024" [workspace] [dependencies] +# The SSZ encoder and the native validation check, shared with ethrex-fixtures: the block +# is sourced here, encoded there. +ethrex-ssz-input = { path = "../ethrex-ssz-input" } # Pinned to the SAME ethrex rev as the guest so the generated SSZ input matches # what the stateless guest deserializes. ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common", default-features = false, features = ["secp256k1"] } diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index afca64fb4..14233b60a 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -8,15 +8,9 @@ use ethrex_common::constants::{DEFAULT_REQUESTS_HASH, EMPTY_BLOCK_ACCESS_LIST_HA use ethrex_common::types::Block; use ethrex_common::types::block_access_list::BlockAccessList; use ethrex_common::types::block_execution_witness::RpcExecutionWitness; -use ethrex_common::types::stateless_ssz::{ - Bytes20, ExecutionPayload, ExecutionRequests, LogsBloom, NewPayloadRequest, - STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, -}; use ethrex_config::networks::Network; use ethrex_crypto::NativeCrypto; -use ethrex_guest_program::l1::run_stateless_guest; -use libssz::SszEncode; -use libssz_types::{ProgressiveList, SszList, SszVector}; +use ethrex_ssz_input::{build_stateless_input, validate_natively}; use serde::Deserialize; /// The subset of an `ethrex-replay` cache needed by the stateless SSZ input. @@ -48,55 +42,6 @@ struct BlockSummary { gas_used: u64, } -fn empty_execution_requests() -> ExecutionRequests { - ExecutionRequests { - deposits: ProgressiveList::new(), - withdrawals: ProgressiveList::new(), - consolidations: ProgressiveList::new(), - builder_deposits: ProgressiveList::new(), - builder_exits: ProgressiveList::new(), - } -} - -fn ssz_witness( - witness: &RpcExecutionWitness, -) -> Result> { - let state = witness - .state - .iter() - .enumerate() - .map(|(i, bytes)| { - SszList::try_from(bytes.to_vec()) - .map_err(|e| format!("witness state[{i}] is too large: {e:?}")) - }) - .collect::, _>>()?; - let codes = witness - .codes - .iter() - .enumerate() - .map(|(i, bytes)| { - SszList::try_from(bytes.to_vec()) - .map_err(|e| format!("witness codes[{i}] is too large: {e:?}")) - }) - .collect::, _>>()?; - let headers = witness - .headers - .iter() - .enumerate() - .map(|(i, bytes)| { - SszList::try_from(bytes.to_vec()) - .map_err(|e| format!("witness headers[{i}] is too large: {e:?}")) - }) - .collect::, _>>()?; - - Ok(SszExecutionWitness { - state: ProgressiveList::from(state), - codes: ProgressiveList::from(codes), - headers: SszList::try_from(headers) - .map_err(|e| format!("witness has too many headers: {e:?}"))?, - }) -} - fn stateless_input_from_cache( cache_path: &str, ) -> Result<(Vec, BlockSummary), Box> { @@ -136,9 +81,9 @@ fn stateless_input_from_cache( let block_access_list_hash = header .block_access_list_hash .ok_or("cache block has no Amsterdam block_access_list_hash")?; - let slot_number = header - .slot_number - .ok_or("cache block has no Amsterdam slot_number")?; + if header.slot_number.is_none() { + return Err("cache block has no Amsterdam slot_number".into()); + } let block_access_list = match cache.block_access_list.as_ref() { Some(bal) => bal.clone(), None if block_access_list_hash == *EMPTY_BLOCK_ACCESS_LIST_HASH => BlockAccessList::new(), @@ -158,97 +103,15 @@ fn stateless_input_from_cache( "cache contains execution requests, but the replay cache has no request data".into(), ); } - let base_fee = header - .base_fee_per_gas - .ok_or("cache block has no base_fee_per_gas")?; - let transactions = block - .body - .transactions - .iter() - .map(|tx| tx.encode_canonical_to_vec().into()) - .collect::>>() - .into(); - let public_keys = block - .body - .transactions - .iter() - .enumerate() - .map(|(i, tx)| { - let key = tx - .public_key(&NativeCrypto) - .map_err(|e| format!("failed to recover public key for transaction {i}: {e}"))? - .ok_or_else(|| format!("transaction {i} has no recoverable signature"))?; - SszVector::try_from(key.to_vec()) - .map_err(|e| format!("public key for transaction {i} is invalid: {e:?}")) - }) - .collect::, String>>()?; - - let withdrawals = block - .body - .withdrawals - .clone() - .unwrap_or_default() - .into_iter() - .map( - |withdrawal| ethrex_common::types::stateless_ssz::Withdrawal { - index: withdrawal.index, - validator_index: withdrawal.validator_index, - address: Bytes20(withdrawal.address.0), - amount: withdrawal.amount, - }, - ) - .collect::>() - .into(); - let logs_bloom: LogsBloom = SszVector::try_from(header.logs_bloom.0.to_vec()) - .map_err(|e| format!("logs bloom is not 256 bytes: {e:?}"))?; - let extra_data = SszList::try_from(header.extra_data.to_vec()) - .map_err(|e| format!("extra data exceeds 32 bytes: {e:?}"))?; - let mut base_fee_bytes = [0u8; 32]; - base_fee_bytes[..8].copy_from_slice(&base_fee.to_le_bytes()); - - let execution_payload = ExecutionPayload { - parent_hash: header.parent_hash.0, - fee_recipient: Bytes20(header.coinbase.0), - state_root: header.state_root.0, - receipts_root: header.receipts_root.0, - logs_bloom, - prev_randao: header.prev_randao.0, - block_number: header.number, - gas_limit: header.gas_limit, - gas_used: header.gas_used, - timestamp: header.timestamp, - extra_data, - base_fee_per_gas: base_fee_bytes, - block_hash: header.compute_block_hash(&NativeCrypto).0, - transactions, - withdrawals, - blob_gas_used: header.blob_gas_used.unwrap_or_default(), - excess_blob_gas: header.excess_blob_gas.unwrap_or_default(), - block_access_list: ethrex_rlp::encode::RLPEncode::encode_to_vec(&block_access_list).into(), - slot_number, - }; - let new_payload_request = NewPayloadRequest { - execution_payload, - versioned_hashes: block - .body - .transactions - .iter() - .flat_map(|tx| tx.blob_versioned_hashes()) - .map(|hash| hash.0) - .collect::>() - .into(), - parent_beacon_block_root: header.parent_beacon_block_root.unwrap_or_default().0, - execution_requests: empty_execution_requests(), - }; - let input = SszStatelessInput { - new_payload_request, - witness: ssz_witness(&cache.witness)?, - chain_id: cache.network.get_genesis()?.config.chain_id, - public_keys: SszPublicKeys::from(public_keys), - }; + if header.base_fee_per_gas.is_none() { + return Err("cache block has no base_fee_per_gas".into()); + } - let mut bytes = STATELESS_INPUT_SCHEMA_ID.to_be_bytes().to_vec(); - input.ssz_append(&mut bytes); + // Everything above sources the block from the cache and rejects what the pinned schema + // cannot express. The encoding itself is `ethrex-ssz-input`, shared with the fixture + // generators, so an ethrex rev bump moves one copy of it instead of three. + let chain_id = cache.network.get_genesis()?.config.chain_id; + let bytes = build_stateless_input(block, &cache.witness, Some(&block_access_list), chain_id)?; Ok((bytes, summary)) } @@ -270,10 +133,8 @@ fn main() -> Result<(), Box> { } let (bytes, summary) = stateless_input_from_cache(&cache_path)?; - let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); - if output.len() != 43 || output[32] == 0 { - return Err("converted stateless input failed native validation".into()); - } + validate_natively(&bytes) + .map_err(|e| format!("converted stateless input failed native validation: {e}"))?; std::fs::write(&out_path, &bytes)?; println!( diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index d755840ac..205a30a77 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -1064,6 +1064,7 @@ dependencies = [ "ethrex-guest-program", "ethrex-l2-rpc", "ethrex-rlp", + "ethrex-ssz-input", "ethrex-storage", "ethrex-trie", "hex", @@ -1276,6 +1277,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "ethrex-ssz-input" +version = "0.1.0" +dependencies = [ + "ethrex-common", + "ethrex-guest-program", + "ethrex-rlp", + "libssz", + "libssz-types", +] + [[package]] name = "ethrex-storage" version = "26.0.0" diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index ce79d4c0d..6125688a1 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -13,6 +13,8 @@ default-run = "ethrex-fixtures" [workspace] [dependencies] +# The SSZ encoder and the native validation check, shared with ethrex-block-converter. +ethrex-ssz-input = { path = "../ethrex-ssz-input" } # Pinned to the SAME ethrex rev as the guest so generated SSZ inputs match what # the stateless guest deserializes. Re-pin every manifest together with # scripts/set_ethrex_rev.sh. diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index 081a70000..d2b92a387 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -26,11 +26,10 @@ use ethrex_common::types::block_execution_witness::{ ExecutionWitness, RpcExecutionWitness, amsterdam_chain_config, decode_witness_headers, }; use ethrex_common::types::{AccountState, Block, ELASTICITY_MULTIPLIER, Genesis}; -use ethrex_fixtures::build_stateless_input; use ethrex_guest_program::crypto::{Crypto, NativeCrypto}; -use ethrex_guest_program::l1::run_stateless_guest; use ethrex_rlp::decode::RLPDecode; use ethrex_rlp::encode::RLPEncode; +use ethrex_ssz_input::{build_stateless_input, validate_natively}; use ethrex_storage::{EngineType, Store}; use ethrex_trie::{Nibbles, Node, NodeRef}; use std::collections::BTreeMap; @@ -344,10 +343,8 @@ async fn main() -> Result<(), Box> { result.block_access_list.as_ref(), CHAIN_ID, )?; - let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); - if output.len() != 43 || output[32] == 0 { - return Err("rebuilt block failed native stateless validation".into()); - } + validate_natively(&bytes) + .map_err(|e| format!("rebuilt block failed native stateless validation: {e}"))?; std::fs::write(&out_path, &bytes)?; // --- 5. report --------------------------------------------------------- diff --git a/tooling/ethrex-fixtures/src/main.rs b/tooling/ethrex-fixtures/src/main.rs index e91ac0e6a..56d5b10d1 100644 --- a/tooling/ethrex-fixtures/src/main.rs +++ b/tooling/ethrex-fixtures/src/main.rs @@ -29,10 +29,8 @@ use ethrex_common::types::{ EIP1559Transaction, ELASTICITY_MULTIPLIER, Genesis, GenesisAccount, Transaction, TxKind, }; use ethrex_common::{Address, H256, U256}; -use ethrex_fixtures::build_stateless_input; -use ethrex_guest_program::crypto::NativeCrypto; -use ethrex_guest_program::l1::run_stateless_guest; use ethrex_l2_rpc::signer::{LocalSigner, Signable, Signer}; +use ethrex_ssz_input::{build_stateless_input, validate_natively}; use ethrex_storage::{EngineType, Store}; use secp256k1::SecretKey; @@ -217,10 +215,8 @@ async fn main() -> Result<(), Box> { .await?; let witness: RpcExecutionWitness = witness.try_into()?; let bytes = build_stateless_input(&block, &witness, block_access_list.as_ref(), chain_id)?; - let output = run_stateless_guest(&bytes, std::sync::Arc::new(NativeCrypto)); - if output.len() != 43 || output[32] == 0 { - return Err("generated stateless fixture failed native validation".into()); - } + validate_natively(&bytes) + .map_err(|e| format!("generated stateless fixture failed native validation: {e}"))?; std::fs::write(&out_path, &bytes)?; let mode_label = match mode { diff --git a/tooling/ethrex-ssz-input/Cargo.toml b/tooling/ethrex-ssz-input/Cargo.toml new file mode 100644 index 000000000..f130ac15e --- /dev/null +++ b/tooling/ethrex-ssz-input/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ethrex-ssz-input" +version = "0.1.0" +edition = "2024" + +# Detached workspace, like its two consumers: it is a path dependency of both, not a +# member of the root workspace, so the ethrex host tree stays out of the main build. +[workspace] + +# Deliberately the smallest set that encodes a stateless input. `ethrex-fixtures` and +# `ethrex-block-converter` both reach the encoder through this crate, and neither pays for +# the other's graph: no blockchain, no storage, no tokio, no RPC. +# +# Pinned to the SAME ethrex rev as the guest so the encoded input matches what the +# stateless guest deserializes. Re-pin every manifest together with +# scripts/set_ethrex_rev.sh. +[dependencies] +ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common" } +ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program" } +ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-rlp" } +# Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. +libssz = "=0.3.0" +libssz-types = "=0.3.0" diff --git a/tooling/ethrex-fixtures/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs similarity index 76% rename from tooling/ethrex-fixtures/src/lib.rs rename to tooling/ethrex-ssz-input/src/lib.rs index 00849e20f..e4cb27f3f 100644 --- a/tooling/ethrex-fixtures/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -1,9 +1,12 @@ -//! Shared SSZ input construction for this crate's two generators. +//! The encoder that turns a block into the schema-prefixed SSZ stateless input, and the +//! check that the guest accepts what came out. //! -//! `build_stateless_input` is the encoder both binaries need — `ethrex-fixtures` for the -//! synthetic blocks it builds from a funded genesis, and `real_block` for a real mainnet -//! block rebuilt as an Amsterdam one. It used to be copied into each, which meant the -//! copies had to be moved together on every ethrex rev bump with nothing enforcing it. +//! Three copies of this used to exist — `ethrex-fixtures` for the synthetic blocks it +//! builds from a funded genesis, its `real_block` binary for a real mainnet block rebuilt +//! as an Amsterdam one, and `ethrex-block-converter` for an ethrex-replay cache — which +//! meant they had to move together on every ethrex rev bump with nothing enforcing it. +//! They differ in where the block comes FROM, not in how it is encoded, so only the +//! sourcing stays in each. use ethrex_common::types::Block; use ethrex_common::types::block_access_list::BlockAccessList; @@ -13,6 +16,7 @@ use ethrex_common::types::stateless_ssz::{ STATELESS_INPUT_SCHEMA_ID, SszExecutionWitness, SszPublicKeys, SszStatelessInput, }; use ethrex_guest_program::crypto::NativeCrypto; +use ethrex_guest_program::l1::run_stateless_guest; use libssz::SszEncode; use libssz_types::{ProgressiveList, SszList, SszVector}; @@ -153,3 +157,29 @@ pub fn build_stateless_input( input.ssz_append(&mut bytes); Ok(bytes) } + +/// The guest's public output: `state_root(32) || successful_validation(1) || +/// chain_id(8) || schema_id(2)`. +pub const GUEST_OUTPUT_LEN: usize = 43; +/// Offset of `successful_validation` within that output. +pub const VALIDATION_FLAG: usize = 32; + +/// Run an encoded input through the stateless guest on the host, and report whether the +/// block validated. +/// +/// This is the check every producer owes its output before writing it: a guest that cannot +/// decode the input does not fail, it commits an all-zero result and exits cleanly, so an +/// unvalidated fixture benchmarks nothing while looking like a 99% improvement. +pub fn validate_natively(bytes: &[u8]) -> Result<(), String> { + let output = run_stateless_guest(bytes, std::sync::Arc::new(NativeCrypto)); + if output.len() != GUEST_OUTPUT_LEN { + return Err(format!( + "guest returned {} bytes, expected {GUEST_OUTPUT_LEN}", + output.len() + )); + } + if output[VALIDATION_FLAG] == 0 { + return Err("the guest rejected the block (successful_validation = 0)".into()); + } + Ok(()) +} From f9e56a65ed6ddb2354550ecc5b15dea6aba976de Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 16:45:07 -0300 Subject: [PATCH 35/46] Seed every ancestor the witness carries into BLOCKHASH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment claimed the cache was seeded with "the ancestor hashes the witness carries", and the code inserted exactly one: the parent. A transaction reading BLOCKHASH at any of the other 255 depths would have seen zero where mainnet gave it a hash, and the fixture would have executed something other than what it claims to reproduce, with nothing to say so. The witness already carries the headers — they are decoded a few lines above to build the execution witness — so the cache is built from them, with the local head overwriting the real ancestor at its own height because that is the block this one builds on. Nothing about the current fixture moves: regenerated, it is byte-for-byte the pinned `08a52e10…`, which says no transaction in this block reads BLOCKHASH past its parent. The seeding is there for the next block someone pins. A disagreement with the guest could not pass silently either — the generator validates the result through it, and a BLOCKHASH the guest resolves differently fails the post-state-root check. --- tooling/ethrex-fixtures/src/bin/real_block.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index d2b92a387..c5f2dc126 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -275,7 +275,16 @@ async fn main() -> Result<(), Box> { gas_ceil: GAS_CEIL, }; let skeleton = create_payload(&payload_args, &store, Bytes::new())?; - let mut block_hash_cache = BTreeMap::new(); + // Every ancestor the witness carries, not just the parent: a transaction reading + // BLOCKHASH at any of the other 255 depths would otherwise see zero where mainnet gave + // it a hash, and the fixture would quietly execute something else. The local head + // overwrites the real ancestor at its own height, because that is the block this one + // builds on. If the guest disagreed with any of it the generator would fail below, at + // the post-state-root check. + let mut block_hash_cache: BTreeMap = decoded_headers + .iter() + .map(|header| (header.number, header.compute_block_hash(&NativeCrypto))) + .collect(); block_hash_cache.insert(head_number, head_hash); let (result, rejected, t8n_error) = blockchain.build_payload_t8n( skeleton, From 28b53074956b54674a556a5689a8cf48c5b862e5 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 16:45:07 -0300 Subject: [PATCH 36/46] Name the guest's first output field for what it is The doc on `GUEST_OUTPUT_LEN` called the first 32 bytes a state root. They are `new_payload_request_root`, the payload request's `hash_tree_root`: ethrex's `SszStatelessValidationResult` is `new_payload_request_root || successful_validation || chain_id || schema_id`, in that order. The post-state root is checked inside `validate_stateless_execution`, whose verdict is the flag this module reads, so the flag is the whole answer and the output carries no root to compare against the block. --- tooling/ethrex-ssz-input/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tooling/ethrex-ssz-input/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs index e4cb27f3f..5052ded35 100644 --- a/tooling/ethrex-ssz-input/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -158,8 +158,14 @@ pub fn build_stateless_input( Ok(bytes) } -/// The guest's public output: `state_root(32) || successful_validation(1) || +/// The guest's public output: the fields of ethrex's `SszStatelessValidationResult` in +/// declaration order — `new_payload_request_root(32) || successful_validation(1) || /// chain_id(8) || schema_id(2)`. +/// +/// Those first 32 bytes are the payload request's `hash_tree_root`, not a state root. The +/// post-state root is checked inside `validate_stateless_execution`, and what the guest +/// publishes about it is the flag at [`VALIDATION_FLAG`] — so the flag is the whole verdict, +/// and there is no root in the output worth comparing against the block. pub const GUEST_OUTPUT_LEN: usize = 43; /// Offset of `successful_validation` within that output. pub const VALIDATION_FLAG: usize = 32; From 19200a9f75158db6c37974dfaafce0b681a480eb Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Mon, 14 Sep 2026 16:45:19 -0300 Subject: [PATCH 37/46] Take the fixture build's whole process tree down, not just its make MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kill "$FIXTURE_PID"` signalled the `make` and nothing under it. Its cargo and rustc children were reparented and kept compiling, which on the shared bench runner is precisely the contention the next batch would have measured — the failure path was leaving behind the load it existed to prevent. Reproduced on a harness that mirrors the launch: signalling the parent alone left the child running; killing the group took both down. The job now leads its own process group (`set -m` around the launch, off again immediately, since job control also changes how the shell signals and reports jobs) and `kill_fixture` signals the group, falling back to the bare PID, then reaps. --- scripts/bench_abba.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 0dd5b69b4..b031bb805 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -146,8 +146,16 @@ fi mkdir -p "$WORK" # --- 1. Guest ELF + fixture (identical for both sides; build once if missing) --- -# Unset PID (WORKLOAD=synthetic) and already-reaped PID both fall through to `|| true`. -kill_fixture() { kill "${FIXTURE_PID:-}" 2>/dev/null || true; } +# Kills the whole process GROUP, not just the `make`: signalling the make alone leaves its +# cargo and rustc children running, reparented and still compiling, which on the shared bench +# runner is exactly the contention the next batch would measure. `set -m` at the launch below +# is what gives the job a group of its own. Unset PID (WORKLOAD=synthetic) and an +# already-reaped one both fall through. +kill_fixture() { + [ -n "${FIXTURE_PID:-}" ] || return 0 + kill -- "-$FIXTURE_PID" 2>/dev/null || kill "$FIXTURE_PID" 2>/dev/null || true + wait "$FIXTURE_PID" 2>/dev/null || true +} if [ ! -f "$ELF_REL" ]; then echo "==> Building ethrex guest ELF (missing)" export SYSROOT_DIR="${SYSROOT_DIR:-$HOME/.lambda-vm-sysroot}" @@ -168,8 +176,13 @@ if [ "$WORKLOAD" = "real" ]; then # nothing else. Nothing before the cycle floor reads the fixture, which is where this # is waited on. echo "==> Building ethrex real-block fixture (from its replay cache; in background)" + # Job control on for this launch only, so the job leads its own process group and + # kill_fixture can take the whole build tree down. Off again immediately: it also changes + # how the shell signals and reports jobs, and nothing else here wants that. + set -m make ethrex-real-block-fixture > "$WORK/fixture_build.log" 2>&1 & FIXTURE_PID=$! + set +m # A failure below must not leave a cargo build running: on the shared bench runner the # next batch would measure against it. No-op once the wait below has reaped it. trap kill_fixture EXIT From 4d0d6a320c42b0c401f417900bd02bc3b2273e3b Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 17:00:10 -0300 Subject: [PATCH 38/46] Check the workload executes before benchmarking A fixture the guest rejects still proves, still verifies and still profiles: since this branch moved the guest to run_stateless_guest, an input whose schema it does not recognise is committed as successful_validation = 0 and exits cleanly instead of aborting. Measured on the bench box, the same guest runs a pre-SSZ fixture in 496 cycles against 3,730,771 for its own 10-transfer fixture, so a whole batch reads as a ~99% improvement, in green, on both sides of an A/B. bench_abba.sh already checked this, but only for the real workload, and it was the only entry point that did. bench_verify.sh, perf_diff.sh and benchmark-pr.yml prove the real block after nothing but the fixture's sha256, which pins the fixture and not the pair: moving only the guest's rev (set_ethrex_rev.sh --guest-only, which is how a benchmark varies the guest across revs) leaves a fixture that still matches its digest and an ELF that may no longer accept it. The synthetic side is worse, because it is generated only when MISSING and its names carry neither the rev nor the schema -- ethrex_bench_20.bin is gitignored, so a pre-bump copy survives a rev bump, a git clean -fd and every git status. So the check moves into scripts/assert_workload_cycles.sh and all four callers use it, for both workloads. The synthetic floor is 100k rather than the real 1M because a small TX_COUNT is a legitimate workload: an empty block ran 0.99M cycles and a 1-transfer block 1.80M, an order of magnitude above a rejected run. A run that never reaches a cycle count at all (a missing file, an execution error, a cli that does not implement a syscall the ELF uses) gets its own message and the tail of its output, because that is a different fault from a rejected input and the two were easy to confuse. --- .github/workflows/benchmark-pr.yml | 11 +++++ scripts/assert_workload_cycles.sh | 78 ++++++++++++++++++++++++++++++ scripts/bench_abba.sh | 48 +++++------------- scripts/bench_verify.sh | 7 +++ scripts/perf_diff.sh | 6 +++ 5 files changed, 114 insertions(+), 36 deletions(-) create mode 100755 scripts/assert_workload_cycles.sh diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index 829bc66f9..a0e64f7f9 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -329,6 +329,17 @@ jobs: make ethrex-real-block-fixture ls -l "$REAL_INPUT" + - name: Check the real block executes before measuring it + if: steps.config.outputs.run_real == 'true' + run: | + # The digest above pins the fixture, not the pair: an ELF whose ethrex rev moved + # on its own (scripts/set_ethrex_rev.sh --guest-only) still matches it, and a + # rejected input is committed as `successful_validation = 0` in ~500 cycles + # rather than failing -- which every run below would then report as a ~99% + # improvement, in green. One execution, both sides: the baseline job proves this + # same ELF and this same fixture (it survives its `git checkout origin/main`). + scripts/assert_workload_cycles.sh ./target/release/cli "$ELF" "$REAL_INPUT" real + - name: Memory growth (PR) id: pr-growth if: steps.config.outputs.run_growth == 'true' diff --git a/scripts/assert_workload_cycles.sh b/scripts/assert_workload_cycles.sh new file mode 100755 index 000000000..2b543c431 --- /dev/null +++ b/scripts/assert_workload_cycles.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Refuse to benchmark a workload the guest rejected. +# +# `run_stateless_guest` cannot fail. On an input whose schema it does not recognise it +# commits `successful_validation = 0` and exits cleanly, so a fixture from before the +# ethrex 26 bump runs 496 cycles instead of aborting -- measured, against 3,730,771 for +# the same guest on its own 10-transfer fixture. Proving that reads as a ~99% improvement, +# in green, on both sides of an A/B. +# +# Every benchmark entry point reuses whatever fixture is already on disk, so on a +# persistent runner this is the common case and not a corner case. The real block is +# pinned twice -- by sha256 in the Makefile and by a filename carrying the block number -- +# but the synthetic names carry neither the rev nor the schema: +# `executor/tests/ethrex_bench_20.bin` is gitignored, so it survives a rev bump, a +# `git clean -fd` and every `git status`, and `ethrex__transfers.bin` is merely +# untracked. The digest pin cannot see the other half of the pair either: moving only the +# guest (`scripts/set_ethrex_rev.sh --guest-only`, which is how a benchmark varies the +# guest across revs) leaves a fixture that still matches its pin and an ELF that may no +# longer accept it. +# +# One cheap execution turns that whole class of mistake -- stale fixture, wrong fork, +# fixture built against another ethrex rev -- into a hard stop. +# +# Usage: scripts/assert_workload_cycles.sh real|synthetic +# +# A standalone script rather than a sourceable function, because +# .github/workflows/benchmark-pr.yml calls it straight from a `run:` block. +set -euo pipefail + +# Far above a rejected run (496 cycles) and far below the smallest honest one. The real +# block is ~37M cycles today. The synthetic floor is an order of magnitude lower because a +# small TX_COUNT is a legitimate workload: a 1-transfer block ran 1.80M cycles before the +# bump and an empty one 0.99M. +MIN_CYCLES_REAL=1000000 +MIN_CYCLES_SYNTHETIC=100000 + +if [ "$#" -ne 4 ]; then + echo "usage: ${0##*/} real|synthetic" >&2 + exit 2 +fi +CLI="$1"; ELF="$2"; INPUT="$3"; WORKLOAD="$4" +case "$WORKLOAD" in + real) FLOOR=$MIN_CYCLES_REAL ;; + synthetic) FLOOR=$MIN_CYCLES_SYNTHETIC ;; + *) echo "ERROR: workload must be 'real' or 'synthetic' (got '$WORKLOAD')." >&2; exit 2 ;; +esac + +# Kept whole instead of piped straight into awk, so a run that never reaches a cycle +# count can be told apart from one that ran and was rejected -- and so its own diagnostic +# survives to be printed. `|| true`: a failing command substitution would otherwise abort +# under `set -e` before either message below, which is fail-closed but silent. +out="$("$CLI" execute "$ELF" --private-input "$INPUT" --cycles 2>&1)" || true +cycles="$(printf '%s\n' "$out" | awk '/^Cycles:/ {print $2}')" +# Every non-numeric answer -- absent, a usage error, several lines -- is "no count". +case "${cycles:-}" in ''|*[!0-9]*) cycles="" ;; esac + +if [ -z "$cycles" ]; then + echo "ERROR: 'cli execute' printed no cycle count, so the run never got as far as being" >&2 + echo " accepted or rejected. That is a different fault -- a missing file, an" >&2 + echo " execution error, a cli that does not implement a syscall this ELF uses." >&2 + echo " cli $CLI" >&2 + echo " ELF $ELF" >&2 + echo " input $INPUT" >&2 + echo " Tail of its output:" >&2 + printf '%s\n' "$out" | tail -5 | sed 's/^/ /' >&2 + exit 1 +fi +if [ "$cycles" -lt "$FLOOR" ]; then + echo "ERROR: the workload executed only $cycles cycles, below the $FLOOR floor for the" >&2 + echo " $WORKLOAD workload. The guest almost certainly rejected the input:" >&2 + echo " ELF $ELF" >&2 + echo " input $INPUT" >&2 + echo " Rebuild the fixture at this ethrex rev: 'make regen-real-block-fixture' for" >&2 + echo " the real block, or delete the synthetic one and let the caller regenerate it" >&2 + echo " (tooling/ethrex-fixtures)." >&2 + exit 1 +fi +echo "==> Workload executes: $cycles cycles ($WORKLOAD floor $FLOOR)" diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index b031bb805..141016d50 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -194,28 +194,6 @@ fi ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")" INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")" -# A workload the guest REJECTS still produces a proof -- of a program that decoded two -# bytes and gave up. `run_stateless_guest` cannot fail: on a schema it does not -# recognise it commits `successful_validation = 0` and exits cleanly, which is what the -# pre-Amsterdam rkyv fixture now does in 496 cycles. Proving that reads as a ~99% -# improvement, in green, on both sides of the A/B. One cheap execution up front turns -# that class of mistake -- stale fixture, wrong fork, fixture built against another -# ethrex rev -- into a hard stop. The floor is far below any real block (the current -# one is ~37M cycles) and far above a rejected one. -# -# Real workload only. The synthetic fixtures are regenerated by the same tool at the -# same rev and are gated twice already (`make check-ethrex-fixture-checksums` plus the -# `successful_validation == 1` assert in tooling/ethrex-tests), while a small TX_COUNT -# legitimately lands near this floor -- a 1-transfer block ran 1.80M cycles before the -# bump, an empty one 0.99M. Applying the floor there would reject valid runs. -# -# Runs after step 2 and reuses `cli_B`: the baseline prover executes the workload just as -# well as a freshly built one, and building a third binary here would be a full extra -# release build -- at the repo root with default features, so sharing neither the -# worktree's target dir nor its feature set. Free on a warm runner, a cold build on paid -# time on a rented GPU box. -MIN_PLAUSIBLE_CYCLES=1000000 - # --- 2. Build (or reuse) both prover binaries --- need_build=0 if [ "${REBUILD:-0}" = "1" ] || [ ! -x "$WORK/cli_A" ] || [ ! -x "$WORK/cli_B" ]; then @@ -279,22 +257,20 @@ if [ "$WORKLOAD" = "real" ]; then fi trap - EXIT echo "==> Fixture ready: $INPUT_REL" - # `|| workload_cycles=""` keeps the failure inside a compound: a bare assignment from a - # failing command substitution aborts under `set -e` before the diagnostic below can - # print, which is fail-closed but silent. - workload_cycles="$("$WORK/cli_B" execute "$ELF" --private-input "$INPUT" --cycles 2>&1 \ - | awk '/^Cycles:/ {print $2}')" || workload_cycles="" - if [ "${workload_cycles:-0}" -lt "$MIN_PLAUSIBLE_CYCLES" ]; then - echo "ERROR: the workload executed only ${workload_cycles:-0} cycles, below the" >&2 - echo " ${MIN_PLAUSIBLE_CYCLES} floor. The guest almost certainly rejected the input:" >&2 - echo " ELF $ELF" >&2 - echo " input $INPUT" >&2 - echo " Rebuild the fixture at this ethrex rev (make regen-real-block-fixture)." >&2 - exit 1 - fi - echo "==> Workload executes: $workload_cycles cycles" fi +# Both workloads, not just `real`: the synthetic fixture is generated only when MISSING +# (above), so a file left by an earlier ethrex rev is reused as-is, and since the bump a +# rejected input is committed rather than aborted. The floors and the reasoning live in +# the script, which the other benchmark entry points share. +# +# Runs after step 2 and reuses `cli_B`: the baseline prover executes the workload just as +# well as a freshly built one, and building a third binary here would be a full extra +# release build -- at the repo root with default features, so sharing neither the +# worktree's target dir nor its feature set. Free on a warm runner, a cold build on paid +# time on a rented GPU box. +"$ROOT/scripts/assert_workload_cycles.sh" "$WORK/cli_B" "$ELF" "$INPUT" "$WORKLOAD" + # --- 3. Interleaved A/B/B/A measurement (fresh CSV -- pre-committed batch) --- # Every prove also records the share of CPU it actually got. On a shared box that # is the difference between a number and a coincidence: measured on the bench diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index 5901f4bae..c8afebd43 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -232,6 +232,13 @@ else echo " cli_A=${SHA_A:0:10} cli_B=${SHA_B:0:10} features=$BENCH_FEATURES" fi +# A fixture the guest REJECTS still proves and still verifies -- a program that read two +# bytes and gave up. The digest check above catches a stale real-block file, but not the +# other half of the pair (an ELF whose ethrex rev moved on its own), and the synthetic +# fixture is generated only when missing, so a pre-bump file is reused as-is. Shared with +# bench_abba.sh and perf_diff.sh; the floors and the reasoning live there. +"$ROOT/scripts/assert_workload_cycles.sh" "$WORK/cli_B" "$ELF" "$INPUT" "$WORKLOAD" + # --- 3. Prove, then interleaved A/B/B/A verify measurement --- # Default: both sides verify ONE shared proof (proved by the baseline), which gives # ABBA the tightest precision (no proof-specific variance). That only works when both diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh index aba7f990b..611e3ea34 100755 --- a/scripts/perf_diff.sh +++ b/scripts/perf_diff.sh @@ -136,6 +136,12 @@ else echo "==> Reusing cached binaries (cli_A=${SHA_A:0:10} cli_B=${SHA_B:0:10})" fi +# Five flamegraphs of a guest that rejected its input look exactly like five flamegraphs +# of one that ran: the profile is just very short. The digest check above pins the +# real-block fixture but not the ELF's ethrex rev, and the synthetic fixture is whatever +# was left on disk. Shared with bench_abba.sh and bench_verify.sh. +"$ROOT/scripts/assert_workload_cycles.sh" "$WORK/cli_B" "$ELF_REL" "$INPUT_REL" "$WORKLOAD" + # --- 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) From e3481c983c25e660c37451a8bda579d48c0227fd Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 17:00:32 -0300 Subject: [PATCH 39/46] Drop the ssz deps the tooling never uses Neither generator references libssz or libssz-types: both reach the encoder through the ethrex-ssz-input path dependency, which declares the same exact =0.3.0 pin and is resolved inside these crates' own workspaces, so the pin they were spelling out arrives anyway. --- tooling/ethrex-block-converter/Cargo.lock | 2 -- tooling/ethrex-block-converter/Cargo.toml | 3 --- tooling/ethrex-fixtures/Cargo.lock | 2 -- tooling/ethrex-fixtures/Cargo.toml | 3 --- 4 files changed, 10 deletions(-) diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index c243d9778..c326518d9 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -844,8 +844,6 @@ dependencies = [ "ethrex-guest-program", "ethrex-rlp", "ethrex-ssz-input", - "libssz", - "libssz-types", "serde", "serde_json", ] diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index 66de00d6d..ddb041798 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -32,8 +32,5 @@ ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0 # Closing it means dropping `ethrex-config` and sourcing `ChainConfig` another way. ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program", default-features = false } -# Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. -libssz = "=0.3.0" -libssz-types = "=0.3.0" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/tooling/ethrex-fixtures/Cargo.lock b/tooling/ethrex-fixtures/Cargo.lock index 205a30a77..649a65fd7 100644 --- a/tooling/ethrex-fixtures/Cargo.lock +++ b/tooling/ethrex-fixtures/Cargo.lock @@ -1068,8 +1068,6 @@ dependencies = [ "ethrex-storage", "ethrex-trie", "hex", - "libssz", - "libssz-types", "secp256k1", "serde_json", "tokio", diff --git a/tooling/ethrex-fixtures/Cargo.toml b/tooling/ethrex-fixtures/Cargo.toml index 6125688a1..a70f5138a 100644 --- a/tooling/ethrex-fixtures/Cargo.toml +++ b/tooling/ethrex-fixtures/Cargo.toml @@ -29,9 +29,6 @@ ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0 ethrex-trie = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-trie" } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -# Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. -libssz = "=0.3.0" -libssz-types = "=0.3.0" serde_json = "1" bytes = "1" secp256k1 = "0.30" From b7b1040244cd4bb6528799dc552b8d9323ba7e9c Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 17:00:33 -0300 Subject: [PATCH 40/46] Read the guest output shape from one place ethrex-tests asserted on a 43-byte output and on byte 32 as literals while ethrex-ssz-input already exported GUEST_OUTPUT_LEN and VALIDATION_FLAG for exactly that, so the guest's output layout was written down twice and only one copy was next to the encoder that produces it. Depending on that crate also replaces the hand-declared libssz pin, which this crate does not use and only carried to keep ethrex-guest-program's ^0.3 from floating. empty_execution_requests becomes private with it: one caller, in its own module. --- tooling/ethrex-ssz-input/src/lib.rs | 2 +- tooling/ethrex-tests/Cargo.lock | 14 +++++++++++++- tooling/ethrex-tests/Cargo.toml | 5 ++++- tooling/ethrex-tests/tests/ethrex.rs | 12 ++++++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/tooling/ethrex-ssz-input/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs index 5052ded35..f7af02736 100644 --- a/tooling/ethrex-ssz-input/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -20,7 +20,7 @@ use ethrex_guest_program::l1::run_stateless_guest; use libssz::SszEncode; use libssz_types::{ProgressiveList, SszList, SszVector}; -pub fn empty_execution_requests() -> ExecutionRequests { +fn empty_execution_requests() -> ExecutionRequests { ExecutionRequests { deposits: ProgressiveList::new(), withdrawals: ProgressiveList::new(), diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 4a27fae68..db591fe3e 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -714,6 +714,7 @@ dependencies = [ "libssz-types", "lru", "once_cell", + "rayon", "rkyv", "rustc-hash", "secp256k1", @@ -814,13 +815,24 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ethrex-ssz-input" +version = "0.1.0" +dependencies = [ + "ethrex-common", + "ethrex-guest-program", + "ethrex-rlp", + "libssz", + "libssz-types", +] + [[package]] name = "ethrex-tests" version = "0.1.0" dependencies = [ "ethrex-guest-program", + "ethrex-ssz-input", "executor", - "libssz", ] [[package]] diff --git a/tooling/ethrex-tests/Cargo.toml b/tooling/ethrex-tests/Cargo.toml index 9912b7dd7..639d85ce6 100644 --- a/tooling/ethrex-tests/Cargo.toml +++ b/tooling/ethrex-tests/Cargo.toml @@ -13,7 +13,10 @@ executor = { path = "../../executor" } # same SSZ stateless input and output. Re-pin every manifest together with # scripts/set_ethrex_rev.sh. ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program" } -libssz = "=0.3.0" +# For GUEST_OUTPUT_LEN/VALIDATION_FLAG, and for the `=0.3.0` libssz pin it carries: this +# crate does not use libssz itself, but `ethrex-guest-program` requires `^0.3`, and the +# guest's output shape must not be spelled out a second time here. +ethrex-ssz-input = { path = "../ethrex-ssz-input" } # Match the root workspace's optimized dev profile: these tests execute the VM. [profile.dev] diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index 8f1d74eef..d53e74a9e 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -5,6 +5,7 @@ use ethrex_guest_program::crypto::NativeCrypto; use ethrex_guest_program::l1::run_stateless_guest; +use ethrex_ssz_input::{GUEST_OUTPUT_LEN, VALIDATION_FLAG}; use executor::elf::Elf; use executor::vm::execution::{Executor, ExecutorError, ReturnValues}; use std::sync::Arc; @@ -24,8 +25,15 @@ fn run_program_without_expect( fn native_output(inputs: &[u8]) -> Vec { let output = run_stateless_guest(inputs, Arc::new(NativeCrypto)); - assert_eq!(output.len(), 43, "unexpected stateless output length"); - assert_eq!(output[32], 1, "native stateless validation failed"); + assert_eq!( + output.len(), + GUEST_OUTPUT_LEN, + "unexpected stateless output length" + ); + assert_eq!( + output[VALIDATION_FLAG], 1, + "native stateless validation failed" + ); output } From c774293d5cb48f7e29a584c75f5e541e9389e3b7 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 17:00:33 -0300 Subject: [PATCH 41/46] Drop the blob schedule copy that does nothing amsterdam_chain_config ends in ..Default::default(), so the config it installs already carries BlobSchedule::default(): cancun, prague, osaka, bpo1 and bpo2 filled, amsterdam None. BlobSchedule's serde defaults are per field and are those same constructors, and genesis.json's two entries are literally the default values, so preserving its schedule across the assignment copied the default over the default. No amsterdam entry is needed either way: amsterdam_chain_config activates BPO1 and BPO2, and EIP-7892 has named forks inherit the highest activated BPO entry, so get_fork_blob_schedule resolves BPO2's 14/21/11684671. The comment now says that, instead of claiming only the chain id and the fee floor change when the whole fork schedule is replaced. Regenerating from the cache after the deletion gives the fixture digest already pinned in the Makefile, byte for byte. --- tooling/ethrex-fixtures/src/bin/real_block.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index c5f2dc126..809d3341b 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -130,13 +130,17 @@ async fn main() -> Result<(), Box> { .collect(); // --- 2. a local Amsterdam chain to host the block ---------------------- // The committed genesis carries the system contracts and the two EIP-8282 - // predeploys Amsterdam needs; only the chain id and fee floor change. The - // block's own accounts do NOT go in the alloc — they are installed into the - // tries below, which needs no key preimages. + // predeploys Amsterdam needs; its fork schedule is replaced wholesale by the + // one the guest itself uses, so the two cannot drift. That includes the blob + // schedule: `amsterdam_chain_config` activates BPO1/BPO2, and EIP-7892 has + // Amsterdam inherit the highest activated BPO entry (target 14 / max 21), + // which is what `get_fork_blob_schedule` resolves. Carrying genesis.json's + // own `blobSchedule` over it would be a no-op — its two entries are the + // serde defaults — so it is not carried over at all. The block's accounts do + // NOT go in the alloc either; they are installed into the tries below, which + // needs no key preimages. let mut genesis: Genesis = serde_json::from_str(GENESIS_JSON)?; - let blob_schedule = genesis.config.blob_schedule; genesis.config = amsterdam_chain_config(CHAIN_ID); - genesis.config.blob_schedule = blob_schedule; // `REAL_BLOCK_FORK=osaka` executes the same block under the rules it was // built for. The guest cannot consume the result (it only decodes the // Amsterdam schema), so this exists purely to attribute a gas difference to From 83762b36b23b37eed9ece215afa0f7659a4ac982 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 16 Sep 2026 17:00:33 -0300 Subject: [PATCH 42/46] Re-pin the docs the ssz migration left stale The sixth manifest is ethrex-ssz-input, so set_ethrex_rev.sh re-pins six and not five (the five lockfiles are unchanged -- that crate has none of its own, being a path dependency of the two that do). Its header still described the rkyv input the converter writes and a skew that decodes as the all-zero default; the skew now reports itself as successful_validation = 0 after ~500 cycles. And the converter job's comment advertised a crypto-parity test and a reproducibility digest that no longer exist: both remaining tests assert a rejection, which is all a pre-Amsterdam cache can produce. --- .github/workflows/ethrex-block-converter.yml | 14 ++++++++------ executor/tests/README.md | 4 ++-- scripts/set_ethrex_rev.sh | 11 ++++++----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index 899a3ea01..e60d761e7 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -60,12 +60,14 @@ jobs: workspaces: | 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 - # `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. + # Downloads the pinned ethrex-replay cache (the converter's test input) and runs + # the crate's tests. Both of them assert a REJECTION, because every published + # cache is pre-Amsterdam and the one path that can succeed has no cache to run on + # yet: they pin the Amsterdam gate and the unmappable-network gate, and until such + # a cache exists the job's value is that the crate still compiles and still refuses + # against the pinned ethrex rev. 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 diff --git a/executor/tests/README.md b/executor/tests/README.md index 12309f39a..f7b12149f 100644 --- a/executor/tests/README.md +++ b/executor/tests/README.md @@ -13,7 +13,7 @@ https://github.com/lambdaclass/ethrex.git 8effcb0671c5d0b12fe0161ea37c174ec4466b6a ``` -Five manifests carry that pin, not one. `scripts/set_ethrex_rev.sh --show` prints +Six manifests carry that pin, not one. `scripts/set_ethrex_rev.sh --show` prints it and fails if they ever disagree. The generator enables Amsterdam in its synthetic genesis and includes the two @@ -43,7 +43,7 @@ regenerated and checksummed with them — a rev bump makes every one of these undecodable, not just the three the executor reads. To regenerate after an ethrex rev bump, re-pin with -`scripts/set_ethrex_rev.sh <40-char-sha>` (all five manifests at once), regenerate +`scripts/set_ethrex_rev.sh <40-char-sha>` (all six manifests at once), regenerate the five locks, then run `make regen-ethrex-fixtures`. The checksums below are refreshed by that same run, so they catch a hand-edited diff --git a/scripts/set_ethrex_rev.sh b/scripts/set_ethrex_rev.sh index fe6520b4b..556435f83 100755 --- a/scripts/set_ethrex_rev.sh +++ b/scripts/set_ethrex_rev.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # Re-pin every ethrex git dependency in this repo to one rev, atomically. # -# The guest and the host-side tooling exchange ethrex types (the rkyv stateless -# input the converter writes and the guest decodes), so a rev that differs +# The guest and the host-side tooling exchange ethrex types (the SSZ stateless +# input the fixture tooling writes and the guest decodes), so a rev that differs # between them is not a version skew that fails to build -- it is a fixture the -# guest silently decodes as the all-zero default. Keeping every pin on one rev -# is what makes that impossible, which is why this rewrites all of them together -# rather than leaving them to be bumped by hand. +# guest does not recognise, which it reports by committing +# `successful_validation = 0` after ~500 cycles instead of failing. Keeping every +# pin on one rev is what makes that impossible, which is why this rewrites all of +# them together rather than leaving them to be bumped by hand. # # Usage: # scripts/set_ethrex_rev.sh <40-char-sha> re-pin everything From 20d73269d4910bbaec176409eff5d4ae632c9b76 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 17 Sep 2026 11:57:30 -0300 Subject: [PATCH 43/46] Tidy the ssz tooling and cover two test gaps --- .github/workflows/benchmark-pr.yml | 2 +- .github/workflows/ethrex-block-converter.yml | 14 +++--- bench_vs/README_ethrex.md | 15 +++--- bench_vs/run_ethrex.sh | 4 +- tooling/ethrex-block-converter/Cargo.lock | 2 - tooling/ethrex-block-converter/Cargo.toml | 25 ++++----- tooling/ethrex-block-converter/README.md | 19 +++++-- tooling/ethrex-block-converter/src/main.rs | 35 +++++++++++++ tooling/ethrex-ssz-input/Cargo.toml | 7 +++ tooling/ethrex-ssz-input/src/lib.rs | 53 ++++++++++++++++++++ 10 files changed, 140 insertions(+), 36 deletions(-) diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index a0e64f7f9..b0b3ba1f7 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -490,7 +490,7 @@ jobs: echo "real_all_times=$ALL_TIMES" echo "real_input=$(basename "$REAL_INPUT")" # The basename is derived from network+block, so it is a complete key for - # WHICH block but not for which bytes: the archived ProgramInput layout moves + # WHICH block but not for which bytes: the archived input's layout moves # with the ethrex rev, so one basename can name two byte streams. The digest # is what makes the comparison guard below able to see a fixture swap. echo "real_input_sha=$(sha256sum "$REAL_INPUT" | cut -c1-12)" diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index e60d761e7..fd52e3b66 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -61,13 +61,13 @@ jobs: tooling/ethrex-block-converter -> target # Downloads the pinned ethrex-replay cache (the converter's test input) and runs - # the crate's tests. Both of them assert a REJECTION, because every published - # cache is pre-Amsterdam and the one path that can succeed has no cache to run on - # yet: they pin the Amsterdam gate and the unmappable-network gate, and until such - # a cache exists the job's value is that the crate still compiles and still refuses - # against the pinned ethrex rev. 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. + # the crate's tests: the Amsterdam gate, the unmappable-network gate, and the + # conversion itself over a cache patched to carry the Amsterdam fields, since every + # published cache predates the fork and would be refused. So what the job pins is + # that the crate still compiles, still refuses what it must and still encodes what + # it accepts, against the pinned ethrex rev. 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 covers that. - name: Run converter tests run: make test-ethrex-real-block-converter diff --git a/bench_vs/README_ethrex.md b/bench_vs/README_ethrex.md index 53be32a98..e52b13b97 100644 --- a/bench_vs/README_ethrex.md +++ b/bench_vs/README_ethrex.md @@ -9,15 +9,16 @@ different block inputs: | empty block | `executor/tests/ethrex_empty_block.bin` | ~184k | | 1 transaction (plain ETH transfer) | `executor/tests/ethrex_simple_tx.bin` | ~4.4M | -Each input is a serialized `ProgramInput` (the block + its execution witness, -rkyv-encoded) for the ethrex commit pinned (as `rev`) in -`executor/programs/rust/ethrex/Cargo.toml`. The guest reads it via -`get_private_input()` and runs ethrex's `execution_program`. +Each input is a serialized `SszStatelessInput` (the block + its execution witness, +SSZ-encoded) for the ethrex commit pinned (as `rev`) in +`executor/programs/rust/ethrex/Cargo.toml`. The guest borrows it in place via +`get_private_input_slice()` and runs ethrex's `run_stateless_guest`. Both the fixtures and those instruction counts are therefore a function of that pin: the -counts above were measured at `156cb8d6` and have not been re-measured since the -`797df554` bump, which regenerated both fixtures. Treat them as an order of magnitude, -and re-run the benchmark for real numbers. +counts above were measured at `156cb8d6` and have not been re-measured since. Two bumps +have landed on top — `2cb18b0b` (25.0.0) and `8effcb06` (26.0.0) — and between them the +wire format went from rkyv to SSZ and both fixtures were regenerated. Treat the counts as +an order of magnitude, and re-run the benchmark for real numbers. The timing window is **single-shot end-to-end prove** (ELF load + execution + trace build + AIR construction + STARK prove); it **excludes** verification. diff --git a/bench_vs/run_ethrex.sh b/bench_vs/run_ethrex.sh index b323d5866..377e12cd2 100755 --- a/bench_vs/run_ethrex.sh +++ b/bench_vs/run_ethrex.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Benchmark: Lambda VM proving ethrex blocks. # -# Proves each block in BLOCKS (an ethrex guest ELF + a serialized ProgramInput -# private input) and reports single-shot end-to-end proving time and cycle count. +# Proves each block in BLOCKS (an ethrex guest ELF + an SSZ-encoded stateless +# input) and reports single-shot end-to-end proving time and cycle count. # Add a block by appending a "label|input_basename" entry to BLOCKS — the input # file must live in executor/tests/. # diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock index c326518d9..d7939e612 100644 --- a/tooling/ethrex-block-converter/Cargo.lock +++ b/tooling/ethrex-block-converter/Cargo.lock @@ -841,8 +841,6 @@ dependencies = [ "ethrex-common", "ethrex-config", "ethrex-crypto", - "ethrex-guest-program", - "ethrex-rlp", "ethrex-ssz-input", "serde", "serde_json", diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml index ddb041798..f46413b03 100644 --- a/tooling/ethrex-block-converter/Cargo.toml +++ b/tooling/ethrex-block-converter/Cargo.toml @@ -13,24 +13,25 @@ ethrex-ssz-input = { path = "../ethrex-ssz-input" } # Pinned to the SAME ethrex rev as the guest so the generated SSZ input matches # what the stateless guest deserializes. ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common", default-features = false, features = ["secp256k1"] } +# Source of the chain id, through `Network::get_genesis`. +# +# CAVEAT: this graph does NOT reproduce the guest's precompile surface, and this dep is +# why. It 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 links a +# working c-kzg that the guest does not have; verify with +# `cargo tree -e features -i ethrex-crypto`. c-kzg is the whole gap now — malachite +# modexp and `ark-ff/asm` are on both sides since ethrex-trie's `std` default. Closing +# it means dropping this dep and sourcing the chain id another way. What covers the gap +# meanwhile is the real-block usability screen in +# .github/workflows/ethrex-block-converter.yml, which runs against the guest's own +# feature set. ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-config", default-features = false } # Provides `NativeCrypto`, the host `Crypto` implementation used to recover # transaction keys, hash the BAL, and validate the generated stateless input. # Already in the graph via `ethrex-config` -> `ethrex-p2p`, so this adds no # compilation; the line exists because the bin build needs its own. ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-crypto", default-features = false, features = ["secp256k1"] } -ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-rlp" } -# 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 that the guest does not have; -# verify with `cargo tree -e features -i ethrex-crypto`. c-kzg is the whole gap now — -# malachite modexp and `ark-ff/asm` are on both sides since ethrex-trie's `std` default. -# Closing it means dropping `ethrex-config` and sourcing `ChainConfig` another way. -ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md index 5f3197ea7..f0a54cf61 100644 --- a/tooling/ethrex-block-converter/README.md +++ b/tooling/ethrex-block-converter/README.md @@ -55,8 +55,17 @@ artifacts and are incompatible with the pinned ethrex. ## What the tests cover -Both tests assert a rejection. The pinned Hoodi cache predates Amsterdam and is -refused rather than rewritten, and an unmappable network is refused too. The -success path — cache in, valid SSZ out — has no automated coverage: it needs a -cache from a network that runs Amsterdam, and none is published yet. Until one is, -the encoder is exercised by hand through the `cargo run` above. +Two of the three tests assert a rejection: the pinned Hoodi cache predates +Amsterdam and is refused rather than rewritten, and an unmappable network is +refused too. + +The third covers the success path, with the cache it has: the Hoodi one patched to +carry the two fields the Amsterdam gate requires — a `blockAccessListHash` set to +the empty-BAL hash, so the BAL branch resolves without a raw list, and a +`slotNumber`. That exercises this crate's own work, which is to read a cache, +screen it and hand the block to the encoder. It does not cover the block's +validity: a patched header no longer matches its own hash, so the test asserts on +the encoding and the summary and not on `validate_natively`, which `main` runs and +which only a cache from a network that runs Amsterdam can satisfy. None is +published yet, so that last step is still exercised by hand through the `cargo run` +above. diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs index 14233b60a..70757d1bb 100644 --- a/tooling/ethrex-block-converter/src/main.rs +++ b/tooling/ethrex-block-converter/src/main.rs @@ -172,6 +172,41 @@ mod tests { ); } + /// The success path has no cache to run on: every published replay cache is + /// pre-Amsterdam, which is exactly what the rejection above asserts. So this one is + /// built -- the checked-in cache with the two fields the gate requires, and the + /// empty-BAL hash so that branch resolves without a raw list. What it covers is this + /// file's own work (read the cache, screen it, hand the block to the encoder), not + /// the block's validity: a patched header no longer matches its own hash, so the + /// assertions are on the encoding and the summary rather than on `validate_natively`, + /// which `main` runs and which only a real Amsterdam cache can satisfy. + #[test] + fn an_amsterdam_shaped_cache_is_converted() { + let mut cache: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(CACHE).expect(CACHE_MISSING)).unwrap(); + let header = &mut cache["blocks"][0]["header"]; + header["blockAccessListHash"] = + serde_json::json!(format!("{:#x}", *EMPTY_BLOCK_ACCESS_LIST_HASH)); + header["slotNumber"] = serde_json::json!("0x1"); + + let path = + std::env::temp_dir().join(format!("ethrex_amsterdam_{}.json", std::process::id())); + std::fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap(); + let result = stateless_input_from_cache(path.to_str().unwrap()); + std::fs::remove_file(&path).ok(); + + let (bytes, summary) = result.expect("a cache carrying the Amsterdam fields must convert"); + assert!(!bytes.is_empty(), "the encoder produced no output"); + assert_eq!(summary.blocks, 1); + assert_eq!( + summary.transactions, + cache["blocks"][0]["body"]["transactions"] + .as_array() + .map_or(0, Vec::len), + "the summary does not describe the block it converted" + ); + } + #[test] fn unmappable_network_is_rejected() { let mut cache: serde_json::Value = diff --git a/tooling/ethrex-ssz-input/Cargo.toml b/tooling/ethrex-ssz-input/Cargo.toml index f130ac15e..b54026180 100644 --- a/tooling/ethrex-ssz-input/Cargo.toml +++ b/tooling/ethrex-ssz-input/Cargo.toml @@ -16,6 +16,13 @@ edition = "2024" # scripts/set_ethrex_rev.sh. [dependencies] ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-common" } +# `run_stateless_guest` and `NativeCrypto`, which is what `validate_natively` runs the +# encoded input through. Deliberately WITH its defaults (`default = ["secp256k1"]`), +# unlike the guest, which declares the same crate with `default-features = false` and +# routes ecrecover through the ECSM precompile anyway: this side is the host reference, +# so it takes the native backend. Note the choice is the whole workspace's, not this +# manifest's alone -- cargo unifies features, so a consumer declaring this crate with +# `default-features = false` would not switch them off. ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-guest-program" } ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex.git", rev = "8effcb0671c5d0b12fe0161ea37c174ec4466b6a", package = "ethrex-rlp" } # Exact pin: keep the SSZ implementation in sync with the guest's ethrex rev. diff --git a/tooling/ethrex-ssz-input/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs index f7af02736..958e7b5f3 100644 --- a/tooling/ethrex-ssz-input/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -189,3 +189,56 @@ pub fn validate_natively(bytes: &[u8]) -> Result<(), String> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use ethrex_common::types::stateless_ssz::SszStatelessValidationResult; + + /// [`GUEST_OUTPUT_LEN`] and [`VALIDATION_FLAG`] are offsets into a struct this crate + /// does not own, and the only thing checking them is `validate_natively`'s length + /// comparison -- which a reordering that kept 43 bytes would pass while the flag + /// moved somewhere else. Encoding the upstream struct pins both, and it is the same + /// encoding the guest publishes: `run_stateless_guest` ends in + /// `SszStatelessValidationResult { .. }.ssz_append(&mut out)`. + /// + /// No CI job runs this crate's tests -- it is a path dependency with no lockfile of + /// its own, and building it standalone would be a second cold build of the ethrex + /// host tree. What covers the same property on every PR is `native_output` in + /// tooling/ethrex-tests, which asserts both constants against real fixtures + /// (`make test-ethrex-offline`). This test is what turns that failure into a + /// localized one: there, a reordering shows up as a committed fixture being rejected. + #[test] + fn output_constants_match_the_upstream_result_layout() { + let mut accepted = Vec::new(); + SszStatelessValidationResult { + successful_validation: true, + ..Default::default() + } + .ssz_append(&mut accepted); + + assert_eq!( + accepted.len(), + GUEST_OUTPUT_LEN, + "the stateless result's encoded length moved" + ); + assert_eq!( + accepted[VALIDATION_FLAG], 1, + "successful_validation is no longer at VALIDATION_FLAG" + ); + // Every other field is zero here, so that byte is the flag rather than merely + // agreeing with it -- a field reordered into this offset would fail here too. + assert_eq!( + accepted.iter().filter(|byte| **byte != 0).count(), + 1, + "another field is non-zero, so the offset no longer identifies the flag" + ); + + // The rejection path returns the Default, which is what makes an input the guest + // cannot decode a clean exit instead of an abort: same length, flag clear. + let mut rejected = Vec::new(); + SszStatelessValidationResult::default().ssz_append(&mut rejected); + assert_eq!(rejected.len(), GUEST_OUTPUT_LEN); + assert_eq!(rejected[VALIDATION_FLAG], 0); + } +} From 5323c1a35133af4f9b6b95105f034d026cad31cd Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 17 Sep 2026 15:17:12 -0300 Subject: [PATCH 44/46] Floor every bench entry point and fix its numbers --- .github/workflows/bench-abba.yml | 6 ++- .github/workflows/benchmark-gpu.yml | 22 ++++++---- .github/workflows/benchmark-pr.yml | 20 +++++---- scripts/assert_workload_cycles.sh | 29 ++++++++++--- scripts/bench_abba.sh | 65 +++++++++++++++++++++++++---- scripts/bench_recursion_cycles.sh | 17 +++++++- scripts/bench_recursion_scaling.sh | 15 +++++++ scripts/bench_verify.sh | 12 +++++- 8 files changed, 152 insertions(+), 34 deletions(-) diff --git a/.github/workflows/bench-abba.yml b/.github/workflows/bench-abba.yml index ede04cadc..7c5d38ccd 100644 --- a/.github/workflows/bench-abba.yml +++ b/.github/workflows/bench-abba.yml @@ -8,7 +8,9 @@ name: Bench ABBA tiebreaker # # Syntax: "/bench-abba [N]", where N is the pair count (default 12). # -# Resolution, from the paired t-test (resolvable 95% delta = t* x sd / sqrt(N)). The +# Resolution, from the paired t-test (resolvable 95% delta = 1.96 x sd / sqrt(N), the +# normal approximation; the exact t* at these pair counts is 10-20% larger, so the +# cells below are floors). The # two columns are not a guess bracketing an unknown: they are the same runner under # two conditions, because its variance is contention and not prover jitter. Over a # 14-prove baseline (tooling/ethrex-fixtures/README.md) wall time and the share of CPU @@ -164,7 +166,7 @@ jobs: # interleaved pairs, and prints the paired-t CI + exact Wilcoxon test. # Pass the head SHA (pinned above) so fork PRs resolve. scripts/bench_abba.sh "$HEAD_SHA" origin/main "$PAIRS" 2>&1 | tee /tmp/abba_out.txt - sed -n '/=== ABBA paired result/,$p' /tmp/abba_out.txt > /tmp/abba_result.txt + sed -n '/^==> Exclusivity:/,$p' /tmp/abba_out.txt > /tmp/abba_result.txt - name: Post result if: always() diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml index 3e867831c..3e83ac151 100644 --- a/.github/workflows/benchmark-gpu.yml +++ b/.github/workflows/benchmark-gpu.yml @@ -57,7 +57,10 @@ env: # 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: + # Measured on the 32,607 MiB card, one prove per setting, on the fixture of the day: +# block 25368371 as an rkyv ProgramInput, which the ethrex 26 bump retired. The VRAM +# and epoch figures follow the epoch size and carry over; the wall times and proof +# sizes are that block's. # # 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 @@ -70,7 +73,7 @@ env: # 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 ~32 GiB of HOST memory on - # a CPU build (measured peak RSS on the 124 GiB calibration box; the CUDA path's host + # a CPU build for the block that was current then (the rebuilt one peaks at 44.81 GiB) (measured peak RSS on the 124 GiB calibration box; the CUDA path's host # peak is a different number, ~36 GB — see the cpu_ram floor below), 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. @@ -238,14 +241,15 @@ jobs: MIN_DRIVER: "580" run: | # cpu_ram filter is in GB. Floor 64 GB, raised from 48 with the rebuilt - # workload: that block peaks at 44.81 GiB (47.0 GB) at epoch 2^22 on the CPU - # path, which on a 48 GB box is 98% of RAM -- not headroom to lose to a - # rental with a slightly hungrier allocator, it is already over. 64 GB restores the ~28% headroom the - # 48 GB floor had for the old block (~36 GB host RSS on the CUDA path, - # measured at main vintage) and matches the CPU runner's own floor. It costs + # workload: that block peaks at 44.81 GiB (48.1 GB) at epoch 2^22 on the CPU + # path, which is more than a 48 GB box has -- the old floor would match boxes + # that cannot run the benchmark at all, never mind survive a rental with a + # hungrier allocator. 64 GB restores the ~25% headroom the 48 GB floor had for + # the old block (~36 GB host RSS on the CUDA path, measured at main vintage) + # and matches the CPU runner's own floor. It costs # offer availability: fewer boxes qualify, so PRICE_CAP may need a look if # the query starts coming back empty. Peak is set by the epoch size, not the - # block, so dropping GPU_REAL_EPOCH_LOG2 to 21 is the other lever (~24 GiB, + # block, so dropping GPU_REAL_EPOCH_LOG2 to 21 is the other lever (~32.8 GiB host, # +14% wall). # Continuation peak is set by the epoch size, not the block, so bigger blocks # don't move it; raising the epoch would (see the epoch table in @@ -541,7 +545,7 @@ jobs: set -o pipefail $SSH "bash -lc \"$REMOTE\"" | tee "$RUNNER_TEMP/abba_out.txt" # Extract the result section for the PR comment (same marker bench-abba.yml uses). - sed -n '/=== ABBA paired result/,$p' "$RUNNER_TEMP/abba_out.txt" > "$RUNNER_TEMP/abba_result.txt" + sed -n '/^==> Exclusivity:/,$p' "$RUNNER_TEMP/abba_out.txt" > "$RUNNER_TEMP/abba_result.txt" - name: Write run summary # Always run so a failure (incl. workflow_dispatch, which has no PR comment step) is diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml index b0b3ba1f7..4479883b4 100644 --- a/.github/workflows/benchmark-pr.yml +++ b/.github/workflows/benchmark-pr.yml @@ -77,9 +77,12 @@ env: # against its 13:1. That is the whole point — a prover change can move the # synthetic number and the real one in opposite directions. # - # Those three counts are deterministic for a given ELF and input, so they are the - # check that the workload is what this job thinks it is; every figure below that - # carries seconds or bytes is machine-dependent and was measured on THIS runner. + # Those three counts are deterministic for a given ELF and input, so they are worth + # recording -- but they are documentation, not a gate: nothing in this job compares a + # run against them. What does check the workload is the fixture's sha256 (Makefile) plus + # the cycle floor a few steps below, which refuses a run the guest rejected. Every + # figure that carries seconds or bytes is machine-dependent and was measured on THIS + # runner. # All three move with the ethrex rev, so re-measure on a bump rather than carrying # them across one. # @@ -101,7 +104,7 @@ env: # needs rkyv `pointer_width_64` in the PROVER's bundle serialisation; a PR branch # predating that fix fails at write time rather than mismeasuring. # - # Epoch 2^22, measured on THIS runner on 2026-09-08 (box idle): mean 125.17 s over + # Epoch 2^22, measured on THIS runner on 2026-09-08 (box idle): mean 125.33 s over # 14 proves, sd 1.58 s, CV 1.26%, 44.81 GiB peak RSS, 790 MB bundle; verifying one # bundle is 12.0 s. # @@ -133,7 +136,9 @@ env: # turn down (2, or 1) before reaching for anything else. # # What 3 runs can actually resolve, from that sd: a two-sided 95% delta of ~2.00%. - # Five per side gets to ~1.00% and ten to ~0.58%, so a sub-2% claim needs `/bench N` + # These are 1.96 x sd x sqrt(2/n): two independent samples, which is what this job + # takes, since the PR side and the baseline side are separate sweeps and not pairs. + # Five per side gets to ~1.56% and ten to ~1.10%, so a sub-2% claim needs `/bench N` # (clamped to 5) or the ABBA tiebreaker, not a re-read of a 3-run table. # `/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, and it @@ -286,9 +291,10 @@ jobs: # Clamp to 1-5. Beyond 5 the single-session cached comparison barely improves # (it can't beat the ~1% drift wall); use /bench-abba for finer deltas — it - # resolves ~0.5% over the same block in ~58 min at its default 12 pairs. + # resolves ~1.0% on a shared box (0.28% on a quiet one) over the same block in ~58 min + # at its default 12 pairs. # At 125.17 s a run this is also the difference between a ~10 min and a ~20 min - # occupancy of the one bench runner. Three runs resolve ~2.00%, five ~1.00%. + # occupancy of the one bench runner. Three runs resolve ~2.00%, five ~1.56%. 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_REAL" RUNS=$BENCH_RUNS_REAL diff --git a/scripts/assert_workload_cycles.sh b/scripts/assert_workload_cycles.sh index 2b543c431..8dc503bd6 100755 --- a/scripts/assert_workload_cycles.sh +++ b/scripts/assert_workload_cycles.sh @@ -27,15 +27,34 @@ # .github/workflows/benchmark-pr.yml calls it straight from a `run:` block. set -euo pipefail -# Far above a rejected run (496 cycles) and far below the smallest honest one. The real -# block is ~37M cycles today. The synthetic floor is an order of magnitude lower because a -# small TX_COUNT is a legitimate workload: a 1-transfer block ran 1.80M cycles before the -# bump and an empty one 0.99M. -MIN_CYCLES_REAL=1000000 +# Far above a rejected run (496 cycles) and far below the smallest honest one. +# +# The real block runs 37.1M cycles today, so 20M is ~45% of margin: enough for a guest +# change that genuinely gets cheaper, and still 40,000x above a rejection. A floor of 1M +# would have tolerated a 97% collapse, which is the only kind of breakage this can see -- +# WHICH block is being proven is pinned by the fixture's sha256 and by the ethrex rev, not +# by a cycle count, so this is not the place to re-pin the workload's identity. +# +# The synthetic floor stays two orders of magnitude lower because a small TX_COUNT is a +# legitimate workload: a 1-transaction block ran 1.80M cycles before the bump and an empty +# one 0.99M, and TX_COUNT is a knob, so nothing here knows which size to expect. +MIN_CYCLES_REAL=20000000 MIN_CYCLES_SYNTHETIC=100000 +# A caller that already has the count (scripts/bench_recursion_scaling.sh records one per +# block size) asks for the floor instead of paying a second execution, so the constants +# above stay the only copy. +if [ "${1:-}" = "--floor" ]; then + case "${2:-}" in + real) echo "$MIN_CYCLES_REAL"; exit 0 ;; + synthetic) echo "$MIN_CYCLES_SYNTHETIC"; exit 0 ;; + *) echo "usage: ${0##*/} --floor real|synthetic" >&2; exit 2 ;; + esac +fi + if [ "$#" -ne 4 ]; then echo "usage: ${0##*/} real|synthetic" >&2 + echo " ${0##*/} --floor real|synthetic" >&2 exit 2 fi CLI="$1"; ELF="$2"; INPUT="$3"; WORKLOAD="$4" diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh index 141016d50..783829980 100755 --- a/scripts/bench_abba.sh +++ b/scripts/bench_abba.sh @@ -48,8 +48,10 @@ # WORKLOAD=synthetic to reproduce a number recorded against that fixture. # # Sizing at WORKLOAD=real, from the paired t-test (resolvable 95% delta = -# t* x sd / sqrt(N)). The two columns are the same runner under two conditions, not a -# guess bracketing an unknown: its variance is contention, so the single-run CV is +# 1.96 x sd / sqrt(N), the normal approximation -- the exact t* at these pair counts is +# 10-20% larger, so read every cell below as a floor). The two columns are the same +# runner under two conditions, not a guess bracketing an unknown: its variance is +# contention, so the single-run CV is # 0.34% across the proves that got the most CPU and 1.26% across all of a 14-prove # baseline. sd of a pair delta is sqrt(2) x that. Keep this table in sync with the one # in .github/workflows/bench-abba.yml: @@ -186,8 +188,16 @@ if [ "$WORKLOAD" = "real" ]; then # A failure below must not leave a cargo build running: on the shared bench runner the # next batch would measure against it. No-op once the wait below has reaped it. trap kill_fixture EXIT -elif [ ! -f "$INPUT_REL" ]; then - echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)" +else + # UNCONDITIONAL, unlike the real block's digest check: these names carry neither the rev + # nor the schema (`ethrex__transfers.bin` is untracked, `ethrex_bench_20.bin` is + # gitignored), so a copy left by an earlier ethrex rev is reused byte for byte and no + # digest anywhere would notice. The floor below catches the rev bumps that make the + # guest REJECT the file; it cannot catch a file that is merely a different valid block. + # Regenerating is what makes that impossible, and it is nearly free: the generator is + # deterministic, so a fixture that was already right is rewritten identically, and the + # cargo build is a no-op on a warm target dir. + echo "==> Generating ethrex ${TX_COUNT}-transfer fixture" ( cd tooling/ethrex-fixtures && cargo build --release ) tooling/ethrex-fixtures/target/release/ethrex-fixtures "$TX_COUNT" "$INPUT_REL" distinct fi @@ -239,6 +249,26 @@ if [ "$need_build" = "1" ]; then echo "$1 $BENCH_FEATURES" > "$WORK/$2.sha" } build_cli "$SHA_B" cli_B + # Surface a fixture-build failure after the FIRST prover build instead of after both. A + # bad cache URL or a broken generator takes seconds to fail while the build takes tens of + # minutes, and on an hourly-billed rental those minutes are paid twice before anything + # says so. Non-blocking on purpose: still running means the parallelism is working, and + # the wait further down handles that case. Reaping here also RETIRES the pid -- the EXIT + # trap must not `kill -- -$FIXTURE_PID` a number the kernel may have handed to someone + # else, and the wait below must not wait on a child that no longer exists. + if [ -n "${FIXTURE_PID:-}" ] && ! kill -0 "$FIXTURE_PID" 2>/dev/null; then + if ! wait "$FIXTURE_PID"; then + # Retire the pid before exiting too: the EXIT trap below would otherwise signal a + # process group this build no longer owns. + unset FIXTURE_PID + echo "ERROR: the real-block fixture could not be built (it failed while the first" >&2 + echo " prover binary was building):" >&2 + cat "$WORK/fixture_build.log" >&2 + exit 1 + fi + FIXTURE_DONE=1 + unset FIXTURE_PID + fi build_cli "$SHA_A" cli_A cleanup # Back to step 1's trap, not `trap - EXIT`: the fixture build is still running and an @@ -250,7 +280,9 @@ else fi if [ "$WORKLOAD" = "real" ]; then - if ! wait "$FIXTURE_PID"; then + # `FIXTURE_DONE` means the build above already reaped it; waiting again would be a wait + # on a non-child, which reports failure for a build that succeeded. + if [ -z "${FIXTURE_DONE:-}" ] && ! wait "$FIXTURE_PID"; then echo "ERROR: the real-block fixture could not be built:" >&2 cat "$WORK/fixture_build.log" >&2 exit 1 @@ -323,8 +355,17 @@ done # the box can give, so a run well under it met a neighbour. No box-specific # constant, which matters because this script also runs on rented 16-32 core GPU # hosts where an absolute percentage means nothing. +# +# ALWAYS prints one `==> Exclusivity:` line, including when there is nothing to report. +# Two reasons. The comment posted by bench-abba.yml and benchmark-gpu.yml starts at this +# marker, and the table below tells the reader to pick a column by it, so a run that +# printed nothing would send them to a line they never see. And silence here is +# ambiguous: a box with no `/usr/bin/time` (the rented GPU images are minimal -- they do +# not even ship python3) records no share at all, which would read exactly like a quiet +# box. +excl="" if [ -s "$CPU_SHARES" ]; then - awk ' + excl="$(awk ' { n++; s[n]=$1; if ($1>mx) mx=$1; if (mn==0 || $1 [$role] reusing cached measuring CLI ($measure_cli)" >&2 fi + # 2c-bis. The blob is only as good as the block it was dumped from. `resolve_block_fixture` + # reuses whatever is on disk (the sizes other than 4tx are gitignored), and a copy from + # before an ethrex rev bump is rejected by this ref's inner guest rather than failing, so + # check the pair now -- after the measuring CLI exists and before any number is recorded. + if [ "$is_block" = 1 ]; then + "$ROOT/scripts/assert_workload_cycles.sh" \ + "$measure_cli" "$wt/executor/program_artifacts/rust/ethrex.elf" \ + "$block_fixture" synthetic >&2 + fi + # 2d. Measure: one deterministic execute --cycles run. Time it (CI feasibility). echo "==> [$role] measuring: $measure_cli execute $(basename "$guest_elf") --private-input --cycles" >&2 local t0 t1 dt out diff --git a/scripts/bench_recursion_scaling.sh b/scripts/bench_recursion_scaling.sh index c868586db..79e772687 100755 --- a/scripts/bench_recursion_scaling.sh +++ b/scripts/bench_recursion_scaling.sh @@ -44,6 +44,10 @@ WORK="$(mktemp -d /tmp/recursion_scaling.XXXXXX)" trap 'rm -rf "$WORK"' EXIT CLI=target/release/cli +# Owned by scripts/assert_workload_cycles.sh, which the other benchmark entry points call +# outright; this sweep already measures the count it would re-measure, so it takes the +# floor and checks its own number. +SYNTHETIC_FLOOR="$(scripts/assert_workload_cycles.sh --floor synthetic)" ART=executor/program_artifacts/recursion ETHREX=executor/program_artifacts/rust/ethrex.elf @@ -74,6 +78,17 @@ for P in $PRESETS; do echo "ERROR: [${P}/${N}tx] inner cycle measurement failed for $FIX" >&2 continue fi + # A count is not enough: since the ethrex 26 bump the guest answers an input it cannot + # decode by committing `successful_validation = 0` and exiting, which reads as a few + # hundred cycles rather than as a failure. These fixtures are gitignored and generated + # only when missing, so a pre-bump copy survives a rev bump on a persistent runner and + # the whole sweep would scale a null workload. Floor from the one place that owns it. + if [ "$ic" -lt "$SYNTHETIC_FLOOR" ]; then + echo "txs=$N preset=$P inner_cycles=REJECTED" >> "$RESULTS" + echo "ERROR: [${P}/${N}tx] $FIX executes only $ic cycles, below the $SYNTHETIC_FLOOR" >&2 + echo " floor: the guest rejected it. Delete it and let this script regenerate it." >&2 + exit 1 + fi echo "==> [${P}/${N}tx] proving inner continuation (epoch=2^${EPOCH_LOG2}) ..." >&2 rm -f /tmp/recursion_input.bin diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh index c8afebd43..a3f3ae903 100755 --- a/scripts/bench_verify.sh +++ b/scripts/bench_verify.sh @@ -189,8 +189,16 @@ if [ "$WORKLOAD" = "real" ]; then # ~35 ms, so there is nothing to gate it on. echo "==> Verifying ethrex real-block fixture (regenerates on a digest miss)" make ethrex-real-block-fixture -elif [ ! -f "$INPUT_REL" ]; then - echo "==> Generating ethrex 20-transfer fixture (missing)" +else + # UNCONDITIONAL, unlike the real block's digest check: these names carry neither the rev + # nor the schema (`ethrex__transfers.bin` is untracked, `ethrex_bench_20.bin` is + # gitignored), so a copy left by an earlier ethrex rev is reused byte for byte and no + # digest anywhere would notice. The floor below catches the rev bumps that make the + # guest REJECT the file; it cannot catch a file that is merely a different valid block. + # Regenerating is what makes that impossible, and it is nearly free: the generator is + # deterministic, so a fixture that was already right is rewritten identically, and the + # cargo build is a no-op on a warm target dir. + echo "==> Generating ethrex 20-transfer fixture" ( cd tooling/ethrex-fixtures && cargo build --release ) tooling/ethrex-fixtures/target/release/ethrex-fixtures 20 "$INPUT_REL" distinct fi From 547790a3688de9422b736b2fecbef430048f97a0 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 17 Sep 2026 15:39:05 -0300 Subject: [PATCH 45/46] Close the low findings and run the tests in CI --- .github/workflows/ethrex-block-converter.yml | 37 +++++++++ .github/workflows/pr_main.yaml | 11 ++- Makefile | 50 +++++++++--- prover/benches/bench_continuation.rs | 2 +- prover/src/tests/page_offset_forgery_poc.rs | 2 +- scripts/render_bench_comment.js | 2 +- scripts/set_ethrex_rev.sh | 8 +- scripts/tests/assert_workload_cycles_test.sh | 80 +++++++++++++++++++ tooling/ethrex-fixtures/README.md | 3 +- tooling/ethrex-fixtures/src/bin/real_block.rs | 62 ++++++++++++-- tooling/ethrex-ssz-input/src/lib.rs | 57 +++++++++++++ tooling/ethrex-tests/tests/ethrex.rs | 13 +-- 12 files changed, 295 insertions(+), 32 deletions(-) create mode 100755 scripts/tests/assert_workload_cycles_test.sh diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml index fd52e3b66..6ee5b6f47 100644 --- a/.github/workflows/ethrex-block-converter.yml +++ b/.github/workflows/ethrex-block-converter.yml @@ -71,6 +71,12 @@ jobs: - name: Run converter tests run: make test-ethrex-real-block-converter + # Shares the converter's target dir (see the target), so this is the encoder's own + # tests and not a second build of the ethrex host tree. They pin the guest output + # layout the fixture producers read, which nothing else in CI covers. + - name: Run the SSZ encoder's tests + run: make test-ethrex-ssz-input + block-usable: name: Real-block usability screen runs-on: ubuntu-latest @@ -120,3 +126,34 @@ jobs: run: | cd tooling/ethrex-tests && \ cargo test --release test_ethrex_real_block_native -- --include-ignored + + # Same key as pr_main.yaml's, so this is normally a restore. The build step below + # runs anyway: the ELF rule is FORCE-driven, so cargo decides, and a restore-keys + # fallback could otherwise hand this job an ELF from another guest. + - name: Cache compiled Rust ELF artifacts and build cache + if: steps.fixture.outputs.present == 'true' + uses: actions/cache@v4 + with: + path: | + executor/program_artifacts/rust + executor/shared_target + key: rust-elf-artifacts-${{ hashFiles('executor/programs/rust/**', 'executor/programs/riscv64im-lambda-vm-elf.json', 'syscalls/**', 'Makefile') }} + restore-keys: | + rust-elf-artifacts- + + - name: Compile the ethrex guest ELF + if: steps.fixture.outputs.present == 'true' + run: make executor/program_artifacts/rust/ethrex.elf + + # The real block THROUGH THE VM, compared against the native reference. This is the + # only job that has both halves -- it builds the fixture above, and the PR gate has + # no fixture to run against -- so until this step existed the block every benchmark + # proves was never once executed by the VM in CI: `test_ethrex_real_block_vm` is + # `#[ignore]`d, `make test-ethrex` skips it by name and the gate's `--skip + # test_ethrex_real_block` substring drops it too. It is 37.1M cycles, seconds of + # work, so the reason it stayed out was the missing fixture and not its cost. + - name: Check the real block through the guest ELF + if: steps.fixture.outputs.present == 'true' + run: | + cd tooling/ethrex-tests && \ + cargo test --release test_ethrex_real_block_vm -- --ignored diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index 7d54319c8..1d57f1302 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -40,8 +40,15 @@ jobs: - name: Check ethrex fixture checksums run: make check-ethrex-fixture-checksums - # Five manifests carry the ethrex rev and they must agree: a skew is not a build - # failure, it is a fixture the guest decodes as the all-zero default. `--show` + # The cycle floor every benchmark entry point calls. Shell, so no cargo test sees + # it, and it is the only thing between a benchmark and a workload the guest + # rejected. Two seconds, no dependencies. + - name: Test the benchmark guards + run: make test-bench-guards + + # Six manifests carry the ethrex rev and they must agree: a skew is not a build + # failure, it is a fixture the guest does not recognise, which it reports by + # committing `successful_validation = 0` after ~500 cycles. `--show` # exits non-zero on MIXED. It reads manifests only; the matching locks are held by # `--locked` on each workspace's own test/build step. - name: Check the ethrex rev is consistent across manifests diff --git a/Makefile b/Makefile index ad68883e3..70d417581 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \ ethrex-real-block-cache ethrex-real-block-converter-cache print-real-block-fixture \ print-real-block-cache-url \ -test-ethrex-real-block-converter regen-real-block-fixture +test-ethrex-real-block-converter regen-real-block-fixture \ +test-ethrex-ssz-input test-bench-guards UNAME := $(shell uname) @@ -305,7 +306,7 @@ test-rust: compile-programs-rust # below. It is no longer what regenerates this fixture. # # ---- 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 REAL_BLOCK_FIXTURE in @@ -418,9 +419,19 @@ endef # generator, so its digest is re-checked here the same way -- hashing 549 KB costs # nothing, and only a MISMATCH pays for a rebuild, which is what keeps a cargo # build off every benchmark's critical path. -ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) +# The generate-if-missing step lives INSIDE the recipe rather than as a file +# prerequisite, so this can tell a fixture it just built from one that was already on +# disk. As a prerequisite it could not: a clean checkout with a stale pin generated the +# fixture, found the digest mismatched, and generated it a second time before erroring -- +# two cold ethrex host builds to report one wrong pin. +ethrex-real-block-fixture: @set -e; \ want="$(ETHREX_REAL_BLOCK_FIXTURE_SHA256)"; \ + just_built=0; \ + if [ ! -f "$(ETHREX_REAL_BLOCK_FIXTURE)" ]; then \ + $(MAKE) regen-real-block-fixture; \ + just_built=1; \ + fi; \ if [ -z "$$want" ]; then \ echo "::warning::ETHREX_REAL_BLOCK_FIXTURE_SHA256 is unset - the fixture is not being verified."; \ exit 0; \ @@ -430,8 +441,10 @@ ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) else echo "fixture: missing sha256sum or shasum for checksum verification" >&2; exit 1; fi; \ sha_of() { $$shacmd "$$1" | awk '{print $$1}'; }; \ if [ "$$(sha_of "$(ETHREX_REAL_BLOCK_FIXTURE)")" = "$$want" ]; then exit 0; fi; \ - echo "fixture $(ETHREX_REAL_BLOCK_FIXTURE) does not match $$want - regenerating."; \ - $(MAKE) regen-real-block-fixture; \ + if [ "$$just_built" = 0 ]; then \ + echo "fixture $(ETHREX_REAL_BLOCK_FIXTURE) does not match $$want - regenerating."; \ + $(MAKE) regen-real-block-fixture; \ + fi; \ got="$$(sha_of "$(ETHREX_REAL_BLOCK_FIXTURE)")"; \ if [ "$$got" != "$$want" ]; then \ echo "ERROR: the freshly generated fixture is $$got, not the pinned $$want." >&2; \ @@ -443,11 +456,11 @@ ethrex-real-block-fixture: $(ETHREX_REAL_BLOCK_FIXTURE) exit 1; \ fi -# No prerequisites on purpose. `ethrex-real-block-cache` is phony (so its digest is -# re-checked on every call), and a phony prerequisite always reads as newer than its -# target, which would rebuild the fixture on every single benchmark invocation. The -# cache is fetched from inside `regen-real-block-fixture` instead, so this recipe runs -# only when the file is genuinely missing. +# Kept for `make executor/tests/ethrex__.bin` by hand; the phony target +# above no longer depends on it (see its comment). No prerequisites on purpose either: +# `ethrex-real-block-cache` is phony, and a phony prerequisite always reads as newer than +# its target, which would rebuild the fixture on every single invocation. The cache is +# fetched from inside `regen-real-block-fixture` instead. $(ETHREX_REAL_BLOCK_FIXTURE): $(MAKE) regen-real-block-fixture @@ -527,7 +540,7 @@ test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE) # there is no artifact to upload. regen-real-block-fixture: ethrex-real-block-cache cd tooling/ethrex-fixtures && \ - cargo run --release --bin real_block -- \ + cargo run --locked --release --bin real_block -- \ ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE) # ethrex host-reference tests live in the detached `tooling/ethrex-tests` @@ -609,6 +622,21 @@ test-syscalls: cd syscalls && cargo test cd syscalls && cargo test --release +# The SSZ encoder is a path dependency with no workspace of its own in any lockfile, so +# no `cargo test` in this repo reaches its tests -- and what they pin is the guest's +# output layout, which belongs to a crate upstream owns. Built into the converter's target +# dir on purpose: its dependency graph is a subset of that one's, so this reuses the build +# instead of paying for a second copy of the ethrex host tree. +test-ethrex-ssz-input: + cd tooling/ethrex-ssz-input && \ + CARGO_TARGET_DIR=$(CURDIR)/tooling/ethrex-block-converter/target cargo test + +# The benchmark entry points are shell, and `make lint` is fmt + clippy over Rust, so +# nothing else in this repo can see a typo in the cycle floor -- a gate that passes +# everything looks exactly like a gate that works. +test-bench-guards: + scripts/tests/assert_workload_cycles_test.sh + # ethrex-crypto is a detached workspace (excluded from the root members), so a # root `cargo test` never runs it. Run it explicitly, like test-syscalls. # Run BOTH profiles deliberately. k256 swaps its FieldElement implementation on diff --git a/prover/benches/bench_continuation.rs b/prover/benches/bench_continuation.rs index c8638346f..7cd4e0f17 100644 --- a/prover/benches/bench_continuation.rs +++ b/prover/benches/bench_continuation.rs @@ -19,7 +19,7 @@ //! //! Env: //! BENCH_PRIVATE_INPUT optional path to a private-input file (e.g. an -//! ethrex ProgramInput .bin). Empty if unset. +//! ethrex SSZ stateless input .bin). Empty if unset. use std::time::Instant; diff --git a/prover/src/tests/page_offset_forgery_poc.rs b/prover/src/tests/page_offset_forgery_poc.rs index 5e2e24d78..56c98585f 100644 --- a/prover/src/tests/page_offset_forgery_poc.rs +++ b/prover/src/tests/page_offset_forgery_poc.rs @@ -513,7 +513,7 @@ fn poc_negative_control_direct_init_tamper_on_preprocessed_page_fails() { /// REACHABILITY on the workload that matters. /// -/// The ethrex block guest reads its ENTIRE `ProgramInput` through +/// The ethrex block guest reads its ENTIRE stateless input through /// `get_private_input()` (`executor/programs/rust/ethrex/src/main.rs:8`), so /// every real block proof carries private-input pages. This asserts it through /// the production function itself — `private_input_page_count` is what the diff --git a/scripts/render_bench_comment.js b/scripts/render_bench_comment.js index cae61028e..60f9d0f0a 100644 --- a/scripts/render_bench_comment.js +++ b/scripts/render_bench_comment.js @@ -90,7 +90,7 @@ 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', + PR_REAL_INPUT: 'ethrex_mainnet_25453112.bin', REAL_RUNS: '3', REAL_TIME_SPREAD: '1.9', REAL_ALL_TIMES: '279.1/281.4/284.4', }; const scenarios = { diff --git a/scripts/set_ethrex_rev.sh b/scripts/set_ethrex_rev.sh index 556435f83..0d66a35e1 100755 --- a/scripts/set_ethrex_rev.sh +++ b/scripts/set_ethrex_rev.sh @@ -28,9 +28,11 @@ cd "$REPO_ROOT" # Every manifest carrying an ethrex git dep. Kept explicit rather than globbed so # a new one has to be added deliberately -- a manifest silently left behind is # exactly the skew described above. -# The guest ELF's own graph. `crypto/ethrex-crypto` is a path dep of BOTH the -# guest and the converter, so moving it moves what the converter links too -- -# which is why a `--guest-only` run must come after the fixture already exists. +# The guest ELF's own graph. `crypto/ethrex-crypto` is the guest's crypto provider +# and nothing under tooling/ references it, so this pair moves the ELF and only the +# ELF -- which is what makes `--guest-only` a guest-side change. It still has to come +# after the fixture exists: the fixture is built by the tooling manifests below, and a +# guest at a different rev may no longer accept what they wrote. GUEST_MANIFESTS=( executor/programs/rust/ethrex/Cargo.toml crypto/ethrex-crypto/Cargo.toml diff --git a/scripts/tests/assert_workload_cycles_test.sh b/scripts/tests/assert_workload_cycles_test.sh new file mode 100755 index 000000000..eeabf583c --- /dev/null +++ b/scripts/tests/assert_workload_cycles_test.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Exercises scripts/assert_workload_cycles.sh, which is the only thing standing between a +# benchmark and a workload the guest rejected. Every branch of it is a hard stop, so a +# typo in one is a gate that passes everything -- and the gate is shell, which no other +# test in this repo would notice. +# +# Fake CLIs instead of the real one: the property under test is the decision, not the +# executor. Real runs of both floors are in the commit that introduced them (a pre-SSZ +# fixture at 496 cycles against 37,137,748 for the real block). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +GUARD="$ROOT/scripts/assert_workload_cycles.sh" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +failures=0 + +fake_cli() { # $1=name $2=body + printf '#!/usr/bin/env bash\n%s\n' "$2" > "$WORK/$1" + chmod +x "$WORK/$1" + echo "$WORK/$1" +} + +check() { # $1=label $2=expected status $3.. = command + local label="$1" want="$2"; shift 2 + local out status + out="$("$@" 2>&1)" && status=0 || status=$? + if [ "$status" != "$want" ]; then + echo "FAIL $label: exit $status, expected $want" + printf '%s\n' "$out" | sed 's/^/ /' + failures=$((failures + 1)) + else + echo "ok $label (exit $status)" + fi +} + +counts_37m="$(fake_cli cli_37m 'echo "Cycles: 37137748"')" +counts_496="$(fake_cli cli_496 'echo "Cycles: 496"')" +counts_1m="$(fake_cli cli_1m 'echo "Cycles: 1000000"')" +counts_none="$(fake_cli cli_none 'echo "Execution failed: UnknownSyscall(18446744073709551615)" >&2; exit 1')" + +check "real block passes the real floor" 0 "$GUARD" "$counts_37m" elf input real +check "rejected input fails the real floor" 1 "$GUARD" "$counts_496" elf input real +check "rejected input fails the synthetic floor" 1 "$GUARD" "$counts_496" elf input synthetic +check "1M passes synthetic but fails real" 0 "$GUARD" "$counts_1m" elf input synthetic +check " (same count, real floor)" 1 "$GUARD" "$counts_1m" elf input real +check "a run with no cycle count is not a pass" 1 "$GUARD" "$counts_none" elf input real +check "an unknown workload is a usage error" 2 "$GUARD" "$counts_37m" elf input realish +check "too few arguments are a usage error" 2 "$GUARD" "$counts_37m" elf input +check "--floor prints the real floor" 0 "$GUARD" --floor real +check "--floor rejects an unknown workload" 2 "$GUARD" --floor nonsense + +# The floors are the reason the pass/fail pairs above land where they do, so a change to +# either without a change here would leave the pairs meaningless. +real_floor="$("$GUARD" --floor real)" +synth_floor="$("$GUARD" --floor synthetic)" +if [ "$real_floor" != "20000000" ] || [ "$synth_floor" != "100000" ]; then + echo "FAIL floors moved: real=$real_floor synthetic=$synth_floor (test pinned 20000000/100000)" + failures=$((failures + 1)) +else + echo "ok floors are 20000000 / 100000" +fi + +# And the diagnostic has to name the fault: "no cycle count" and "below the floor" are +# different repairs, and the first one used to be reported as the second. +# Captured first: the guard exits non-zero by design, and `set -o pipefail` would make +# the pipeline's status the guard's rather than grep's. +diag="$("$GUARD" "$counts_none" elf input real 2>&1 || true)" +if ! printf '%s\n' "$diag" | grep -q "printed no cycle count"; then + echo "FAIL an execution error is not reported as one" + failures=$((failures + 1)) +else + echo "ok an execution error reports the output, not a rejection" +fi + +if [ "$failures" != "0" ]; then + echo "$failures check(s) failed" + exit 1 +fi +echo "all checks passed" diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 057955862..6c366bfe4 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -48,7 +48,8 @@ block touches arrives intact — registers a parent header at the block's real height, replays the transactions in the block's own order through the t8n entry point, and validates the result through the native guest before writing. -Output for mainnet 25453112 (38 transactions, 4,238,394 gas): +Output for mainnet 25453112, whose 38 transactions consumed 4,238,394 gas on mainnet +under Osaka (the `rebuilt` line reports what they consume here, under Amsterdam): ``` installed 132 accounts / 261 storage slots / 132 codes diff --git a/tooling/ethrex-fixtures/src/bin/real_block.rs b/tooling/ethrex-fixtures/src/bin/real_block.rs index 809d3341b..5f5264364 100644 --- a/tooling/ethrex-fixtures/src/bin/real_block.rs +++ b/tooling/ethrex-fixtures/src/bin/real_block.rs @@ -66,7 +66,11 @@ fn collect_leaves(node: &Node, path: Nibbles, out: &mut Vec<(H256, Vec)>) -> }; match node { Node::Branch(branch) => { - let mut unusable = 0; + // A branch carries a value only when some key ends at it, which cannot happen + // in these tries: every key is a 32-byte hash, so no key is a prefix of + // another. Counted rather than ignored, because "cannot happen" is exactly + // what this function reports instead of asserting. + let mut unusable = usize::from(!branch.value.is_empty()); for (i, child) in branch.choices.iter().enumerate() { unusable += descend(child, path.append_new(i as u8), out); } @@ -179,6 +183,15 @@ async fn main() -> Result<(), Box> { let mut installed_accounts = 0usize; let mut installed_slots = 0usize; let mut installed_codes = 0usize; + // Two things the witness can leave out. Neither is an error here -- an account whose + // storage or code this block never touches is simply absent from the witness, and + // installing it is not this tool's job -- but an account that DECLARES either and + // does not get it is installed pointing at nodes the local store does not have. That + // surfaces downstream as a missing-node or missing-code failure if the block reaches + // it, and as nothing at all if it does not, so the counts are reported. + let empty_code_hash = H256(NativeCrypto.keccak256(&[])); + let mut declared_storage_missing = 0usize; + let mut declared_code_missing = 0usize; for (hashed_address, encoded) in &accounts { let mut account = AccountState::decode(encoded) .map_err(|e| format!("decode account {hashed_address:#x}: {e:?}"))?; @@ -194,6 +207,8 @@ async fn main() -> Result<(), Box> { } installed_slots += leaves.len(); account.storage_root = storage_trie.hash(&NativeCrypto)?; + } else if account.storage_root != *ethrex_common::constants::EMPTY_TRIE_HASH { + declared_storage_missing += 1; } if let Some(code) = codes_by_hash.get(&account.code_hash) { store @@ -203,6 +218,8 @@ async fn main() -> Result<(), Box> { )) .await?; installed_codes += 1; + } else if account.code_hash != empty_code_hash { + declared_code_missing += 1; } state_trie.insert(hashed_address.0.to_vec(), account.encode_to_vec())?; installed_accounts += 1; @@ -280,11 +297,17 @@ async fn main() -> Result<(), Box> { }; let skeleton = create_payload(&payload_args, &store, Bytes::new())?; // Every ancestor the witness carries, not just the parent: a transaction reading - // BLOCKHASH at any of the other 255 depths would otherwise see zero where mainnet gave - // it a hash, and the fixture would quietly execute something else. The local head + // BLOCKHASH at one of those depths would otherwise see zero where mainnet gave it a + // hash, and the fixture would quietly execute something else. The local head // overwrites the real ancestor at its own height, because that is the block this one - // builds on. If the guest disagreed with any of it the generator would fail below, at - // the post-state-root check. + // builds on. + // + // The reach is whatever the cache's `headers` field carries, NOT the full 255 depths + // EIP-2935 allows: this cache feeds `build_payload_t8n` here, and the witness the + // guest gets is regenerated separately below. A read deeper than that sees zero on + // both sides, so it cannot make the guest and the native reference disagree -- it + // would just be a block whose BLOCKHASH behaviour is not mainnet's. Repointing to a + // block whose transactions reach further needs that checked, not assumed. let mut block_hash_cache: BTreeMap = decoded_headers .iter() .map(|header| (header.number, header.compute_block_hash(&NativeCrypto))) @@ -332,10 +355,16 @@ async fn main() -> Result<(), Box> { let block = result.payload; let included = block.body.transactions.len(); // The tx mix is the whole reason this fixture exists, so a builder that applied - // fewer than all of them has produced a smaller workload than the block it claims - // to reproduce -- and every guard below still passes, because a block with fewer + // fewer than all of them has produced a smaller block than the one it claims to + // reproduce -- and every guard below still passes, because a block with fewer // transactions is a perfectly valid block. The screen sets the escape: surveying // candidate blocks needs the partial ones reported, not refused. + // + // This counts INCLUSION, not work. A transaction that runs out of gas is included + // with a failed receipt and passes here while executing a fraction of its opcodes, + // and its gas is charged in full either way, so neither this count nor gas_used can + // stand in for the workload. What bounds that is the cycle floor the benchmark + // entry points apply (scripts/assert_workload_cycles.sh). if included != total_txs && std::env::var_os("REAL_BLOCK_ALLOW_DROPS").is_none() { return Err(format!( "the payload builder applied only {included} of {total_txs} transactions; \ @@ -344,6 +373,17 @@ async fn main() -> Result<(), Box> { ) .into()); } + if included != total_txs { + // The bytes carry no record of this: a fixture written with drops is + // indistinguishable from one whose block really had that many transactions, and + // the Makefile's sha256 would happily pin it. Say so where the operator sees it. + eprintln!( + "WARNING: REAL_BLOCK_ALLOW_DROPS is set and {} of {total_txs} transactions \ + were dropped. The output records nothing about that -- use it to screen a \ + candidate block, do NOT pin it as the benchmark fixture.", + total_txs - included + ); + } // --- 4. witness -> SSZ -> native validation ---------------------------- let witness = blockchain @@ -392,6 +432,14 @@ async fn main() -> Result<(), Box> { "installed {installed_accounts} accounts / {installed_slots} storage slots / \ {installed_codes} codes, state root {installed_root:#x}" ); + if declared_storage_missing > 0 || declared_code_missing > 0 { + println!( + "note {declared_storage_missing} account(s) declare storage and \ + {declared_code_missing} declare code the witness did not carry; they are \ + installed pointing at nodes this store does not have. Harmless unless the \ + block reaches them, in which case execution fails below rather than here." + ); + } println!( "rebuilt #{} ({included}/{total_txs} txs, {} gas, {reverted} reverted)", block.header.number, block.header.gas_used diff --git a/tooling/ethrex-ssz-input/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs index 958e7b5f3..be6e6a0e7 100644 --- a/tooling/ethrex-ssz-input/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -195,6 +195,63 @@ mod tests { use super::*; use ethrex_common::types::stateless_ssz::SszStatelessValidationResult; + /// Every artifact this repo generates carries empty `withdrawals`, `extra_data`, + /// `versioned_hashes` and `execution_requests`, so those lists are only ever + /// exercised at length zero and an offset written wrong would encode, decode and + /// validate exactly as well as a correct one. Two of the four can be filled here and + /// read back with the guest's own decoder. + /// + /// The other two cannot. `execution_requests` is hardcoded empty by the encoder (see + /// `empty_execution_requests`, which is what upstream's L2 builder does too), so + /// there is nothing to vary. `versioned_hashes` is derived from blob transactions, + /// which need a signed transaction with a recoverable key; no fixture or cache + /// available to this crate has one, and inventing one belongs with a blob-bearing + /// fixture rather than here. + #[test] + fn non_empty_lists_survive_the_encoding() { + use ethrex_common::types::{Block, Withdrawal}; + use ethrex_common::{Address, Bytes}; + use libssz::SszDecode; + + let bal = BlockAccessList::new(); + let mut block = Block::default(); + block.header.block_access_list_hash = Some(bal.compute_hash(&NativeCrypto)); + block.header.slot_number = Some(7); + block.header.base_fee_per_gas = Some(1); + block.header.extra_data = Bytes::from_static(b"lambda"); + block.body.withdrawals = Some(vec![Withdrawal { + index: 11, + validator_index: 22, + address: Address::from_low_u64_be(33), + amount: 44, + }]); + + let bytes = build_stateless_input(&block, &RpcExecutionWitness::default(), Some(&bal), 1) + .expect("a block carrying withdrawals must encode"); + + // Past the two-byte schema id, this is exactly what the guest's + // `decode_stateless_input` hands to SSZ. + let decoded = SszStatelessInput::from_ssz_bytes(&bytes[2..]) + .expect("the guest's decoder must accept what the encoder wrote"); + let payload = &decoded.new_payload_request.execution_payload; + + assert_eq!(payload.withdrawals.to_vec().len(), 1, "the withdrawal was dropped"); + let withdrawal = &payload.withdrawals.to_vec()[0]; + assert_eq!(withdrawal.index, 11); + assert_eq!(withdrawal.validator_index, 22); + assert_eq!(withdrawal.amount, 44); + assert_eq!( + withdrawal.address.0, + Address::from_low_u64_be(33).to_fixed_bytes(), + "the withdrawal address did not survive" + ); + assert_eq!( + payload.extra_data.to_vec(), + b"lambda".to_vec(), + "extra_data did not survive" + ); + } + /// [`GUEST_OUTPUT_LEN`] and [`VALIDATION_FLAG`] are offsets into a struct this crate /// does not own, and the only thing checking them is `validate_natively`'s length /// comparison -- which a reordering that kept 43 bytes would pass while the flag diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs index d53e74a9e..43ea814f7 100644 --- a/tooling/ethrex-tests/tests/ethrex.rs +++ b/tooling/ethrex-tests/tests/ethrex.rs @@ -108,7 +108,7 @@ fn no_kzg_backend_linked() { ); } -/// Same screen for EIP-2537 (0x0b-0x11), which the 797df554 bump moved behind the +/// Same screen for EIP-2537 (0x0b-0x11), which ethrex moved behind the /// native-only `blst` feature. Unlike the KZG gap these do NOT revert: levm maps /// `CryptoError::Unsupported` to `InternalError`, which aborts the run. Goes red if a /// dependency restores a backend, or if upstream rewords the message. @@ -130,11 +130,14 @@ fn no_bls_backend_linked() { /// 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). +/// fixtures (a ~550 KB 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. +/// Deliberately excluded from the PR CI step, which otherwise runs everything via +/// `--include-ignored`. Not for its runtime -- the block executes in 37.1M cycles, +/// seconds of work -- but because the PR gate has no fixture: it is generated from a +/// replay cache by the ethrex host tooling, which that job does not build. The job +/// that does is the real-block usability screen in +/// .github/workflows/ethrex-block-converter.yml. /// 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"] From 4367f4551357470f789d3483c68a0011bbf7e8ee Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Thu, 17 Sep 2026 15:49:49 -0300 Subject: [PATCH 46/46] Say which graph the native validation runs in --- tooling/ethrex-fixtures/README.md | 8 +++++++- tooling/ethrex-fixtures/src/main.rs | 12 ++++++++++++ tooling/ethrex-ssz-input/src/lib.rs | 23 ++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/tooling/ethrex-fixtures/README.md b/tooling/ethrex-fixtures/README.md index 6c366bfe4..e9163fcc1 100644 --- a/tooling/ethrex-fixtures/README.md +++ b/tooling/ethrex-fixtures/README.md @@ -15,7 +15,13 @@ cargo run --release -- [same|recipients|distinct] ``` `same` uses one funded sender, `recipients` sends to distinct recipients, and -`distinct` uses deterministic funded senders. The standard fixtures are: +`distinct` uses deterministic funded senders. All three emit plain value transfers and +nothing else, which is what keeps the generated fixtures out of reach of a precompile the +guest does not implement — this crate links a working c-kzg that the guest has no backend +for, and only `tooling/ethrex-tests` (no KZG linked) screens for that. Adding a mode that +executes contract code means adding that screen; see the invariant in `src/main.rs`. + +The standard fixtures are: ```bash cargo run --release -- 0 ../../executor/tests/ethrex_empty_block.bin diff --git a/tooling/ethrex-fixtures/src/main.rs b/tooling/ethrex-fixtures/src/main.rs index 56d5b10d1..5fd59ffb0 100644 --- a/tooling/ethrex-fixtures/src/main.rs +++ b/tooling/ethrex-fixtures/src/main.rs @@ -20,6 +20,18 @@ //! //! Pinned to the same ethrex rev as the guest, so the SSZ layout matches what //! the guest deserializes. +//! +//! INVARIANT, and the condition on adding a mode: every mode above emits plain value +//! transfers and nothing else, so a fixture from here cannot reach a precompile the guest +//! does not implement. That matters because this crate's own graph is not the guest's -- +//! it links a working c-kzg (see its Cargo.lock) that the guest has no backend for, so +//! `validate_natively` here would accept a block calling point evaluation (0x0a) that +//! reverts in the VM. What screens for that is `tooling/ethrex-tests`, which links no KZG +//! backend (pinned by its `no_kzg_backend_linked` test) and runs the committed fixtures +//! and the real block through both the native reference and the ELF. The fixtures this +//! binary generates on demand -- `ethrex_bench_.bin`, `ethrex__transfers.bin` -- +//! are NOT in that screen, which is only sound while this invariant holds. A mode that +//! executes contract code has to arrive with one. use bytes::Bytes; use ethrex_blockchain::payload::{BuildPayloadArgs, create_payload}; diff --git a/tooling/ethrex-ssz-input/src/lib.rs b/tooling/ethrex-ssz-input/src/lib.rs index be6e6a0e7..bd164dc34 100644 --- a/tooling/ethrex-ssz-input/src/lib.rs +++ b/tooling/ethrex-ssz-input/src/lib.rs @@ -7,6 +7,16 @@ //! meant they had to move together on every ethrex rev bump with nothing enforcing it. //! They differ in where the block comes FROM, not in how it is encoded, so only the //! sourcing stays in each. +//! +//! Upstream ships an encoder of its own — +//! `build_ssz_stateless_input` in `crates/l2/sequencer/native_rollup/l1_advancer.rs` — +//! and this is deliberately not it. That one serves the L2 native-rollup advancer: it +//! hardcodes `versioned_hashes` and `withdrawals` to empty, which is true of the blocks +//! it builds and not of an L1 block, and it takes its inputs from the sequencer's state +//! rather than from a block plus a witness. What IS worth keeping in step is the field +//! order and the schema prefix, both of which come from the same +//! `ethrex_common::types::stateless_ssz` types this module encodes into, so a layout +//! change upstream is a compile error here rather than a wrong fixture. use ethrex_common::types::Block; use ethrex_common::types::block_access_list::BlockAccessList; @@ -176,6 +186,13 @@ pub const VALIDATION_FLAG: usize = 32; /// This is the check every producer owes its output before writing it: a guest that cannot /// decode the input does not fail, it commits an all-zero result and exits cleanly, so an /// unvalidated fixture benchmarks nothing while looking like a 99% improvement. +/// +/// It runs the guest's code, NOT the guest's dependency graph. Both callers link a working +/// c-kzg through their own trees (the guest links none), so a block whose transactions +/// call point evaluation (0x0a) passes here and reverts in the VM. Accepting a block is +/// therefore a statement about its ENCODING, not about its precompile surface: what +/// covers that is `tooling/ethrex-tests`, which links no KZG backend and compares the +/// native reference against the ELF. pub fn validate_natively(bytes: &[u8]) -> Result<(), String> { let output = run_stateless_guest(bytes, std::sync::Arc::new(NativeCrypto)); if output.len() != GUEST_OUTPUT_LEN { @@ -235,7 +252,11 @@ mod tests { .expect("the guest's decoder must accept what the encoder wrote"); let payload = &decoded.new_payload_request.execution_payload; - assert_eq!(payload.withdrawals.to_vec().len(), 1, "the withdrawal was dropped"); + assert_eq!( + payload.withdrawals.to_vec().len(), + 1, + "the withdrawal was dropped" + ); let withdrawal = &payload.withdrawals.to_vec()[0]; assert_eq!(withdrawal.index, 11); assert_eq!(withdrawal.validator_index, 22);