From 84e31e00db5dc77cf88b33d7847f9462b42f1c85 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Thu, 16 Jul 2026 18:36:23 -0300 Subject: [PATCH 01/13] bench(recursion): measure the verifier at real query counts, over real blocks The recursion cycle benchmark only measured MIN_PROOF_OPTIONS (blowup=2, 1 FRI query) verifying a proof of the empty program - a diagnostic floor, not the production verifier cost (blowup=2 at 128-bit is 219 queries, and real inner proofs are multi-epoch continuation bundles of real blocks). Presets and guests: - Preset::Blowup2 (219 q) / Blowup4 (110 q) - the realistic base-layer regimes - alongside the existing Min/Blowup8; one guest ELF per preset. - bench_vs/lambda/recursion's `continuation` guest feature: verify a whole ContinuationProof bundle in-VM (recursion-cont-.elf), built on the continuation supplied-roots verify path and the zero-copy continuation guest API from the prior two PRs. Benchmark plumbing: - test_dump_recursion_input gains env knobs: RECURSION_DUMP_PRESET, RECURSION_DUMP_INNER_ELF/INPUT (any inner program, e.g. ethrex blocks), RECURSION_DUMP_EPOCH_LOG2 (continuation mode). - scripts/bench_recursion_cycles.sh: preset-aware dump, --release test invocations, per-preset regime labels; /bench-verify now posts min + blowup2 + blowup4 tables. - scripts/bench_recursion_scaling.sh: block-size x preset ladder over the committed ethrex fixtures (1/4/8/16 transfers). - A real-block detailed profile test (blowup=4, 4 transfers) plus a `make test-profile-recursion-block` target, since cycle counts across every preset/block size are already covered by the scripts above. --- .github/workflows/bench-verify.yml | 22 +- Makefile | 32 ++- bench_vs/lambda/recursion/Cargo.toml | 37 +++- bench_vs/lambda/recursion/src/main.rs | 57 +++-- executor/.gitignore | 6 + executor/tests/ethrex_bench_1.bin | Bin 0 -> 13341 bytes executor/tests/ethrex_bench_16.bin | Bin 0 -> 29082 bytes executor/tests/ethrex_bench_4.bin | Bin 0 -> 17371 bytes executor/tests/ethrex_bench_8.bin | Bin 0 -> 21410 bytes prover/src/recursion.rs | 120 ++++++----- prover/src/tests/recursion_smoke_test.rs | 252 ++++++++++++++++++++++- scripts/bench_recursion_cycles.sh | 60 ++++-- scripts/bench_recursion_scaling.sh | 112 ++++++++++ 13 files changed, 594 insertions(+), 104 deletions(-) create mode 100644 executor/tests/ethrex_bench_1.bin create mode 100644 executor/tests/ethrex_bench_16.bin create mode 100644 executor/tests/ethrex_bench_4.bin create mode 100644 executor/tests/ethrex_bench_8.bin create mode 100755 scripts/bench_recursion_scaling.sh diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index baf550bac..e79022082 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -92,9 +92,12 @@ jobs: scripts/bench_verify.sh "$HEAD_SHA" origin/main "$PAIRS" 2>&1 | tee /tmp/verify_out.txt sed -n '/=== Verify ABBA result/,$p' /tmp/verify_out.txt > /tmp/verify_result.txt - # Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main). - # The measurement is one exact `execute --cycles` reading per ref (~1 min total, no - # ABBA). Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli) once, plus + # Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main), + # in three regimes: `min` (blowup=2, 1 query — cheap diagnostic floor) plus the + # realistic full-query base-layer points `blowup2` (219 queries) and `blowup4` + # (110 queries). Each measurement is one exact `execute --cycles` reading per ref + # (no ABBA; the full-query executes are ~1-2 min each). + # Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli) once, plus # per ref a guest build (~10-20 min) and a prover-test build for the blob dump; the # GUEST_TARGET_DIR / HOST_TARGET_DIR below share build-std and the host cargo target # across the two ref worktrees so the second ref is much cheaper (and per-worktree @@ -122,6 +125,19 @@ jobs: set -o pipefail scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main min 2>&1 | tee /tmp/recursion_out.txt sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out.txt > /tmp/recursion_result.txt + # Full-query regimes: the realistic base-layer proofs (the single-query `min` + # regime above is a diagnostic floor, not the real verifier cost). Guest builds + # and worktrees are already cached from the min run, so each adds only a blob + # dump (a full-query prove of the empty inner program, seconds) and one + # ~1-2 min execute per ref. Tolerated failure (else-note): refs predating the + # preset-aware dump test can only measure `min`; the min table above still posts. + for preset in blowup2 blowup4; do + if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then + { echo; sed -n '/=== Recursion-guest cycle/,$p' "/tmp/recursion_out_${preset}.txt"; } >> /tmp/recursion_result.txt + else + { echo; echo "_(${preset} full-query regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt + fi + done - name: Post result if: always() diff --git a/Makefile b/Makefile index 12ac291f5..911edeb17 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \ clean-recursion-elfs clean test test-asm \ test-rust test-ethrex test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \ +test-profile-recursion-block \ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \ test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ @@ -56,13 +57,18 @@ RECURSION_GUESTS := empty fibonacci RECURSION_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/, $(addsuffix .elf, $(RECURSION_GUESTS))) # The recursion verifier itself (bench_vs/lambda/recursion) requires picking -# exactly one of its `min`/`blowup8` Cargo features at build time (fixes the -# inner ProofOptions — see main.rs). Each preset builds its own distinctly -# named [[bin]] (recursion--bench) to its own artifact, via the +# exactly one of its preset Cargo features at build time (fixes the inner +# ProofOptions — see main.rs). Each preset builds its own distinctly named +# [[bin]] (recursion--bench) to its own artifact, via the # define/foreach/eval below rather than the generic %.elf pattern rule. The -# distinct bin names also make the two `cp`s race-free under `make -j`. -RECURSION_VERIFIER_PRESETS := min blowup8 -RECURSION_VERIFIER_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/recursion-, $(addsuffix .elf, $(RECURSION_VERIFIER_PRESETS))) +# distinct bin names also make the per-preset `cp`s race-free under `make -j`. +RECURSION_VERIFIER_PRESETS := min blowup2 blowup4 blowup8 +# Continuation-verifying variants (the guest's `continuation` feature): verify a +# multi-epoch ContinuationProof bundle instead of a monolithic VmProof. Kept to +# the presets the recursion benchmarks actually measure. +RECURSION_CONT_PRESETS := min blowup2 blowup4 +RECURSION_VERIFIER_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/recursion-, $(addsuffix .elf, $(RECURSION_VERIFIER_PRESETS))) \ + $(addprefix $(RECURSION_ARTIFACTS_DIR)/recursion-cont-, $(addsuffix .elf, $(RECURSION_CONT_PRESETS))) # Override with: make ... SYSROOT_DIR=$HOME/.lambda-vm-sysroot # to install the sysroot in a user-writable location and avoid sudo. @@ -215,7 +221,7 @@ $(BENCH_ARTIFACTS_DIR)/%.elf: FORCE | prepare-sysroot $(BENCH_ARTIFACTS_DIR) $(RECURSION_ARTIFACTS_DIR)/%.elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR) $(call build_guest_elf,$(RECURSION_GUESTS_DIR)/$*,$*-bench) -# The recursion verifier's `min`/`blowup8` presets: same crate dir, one +# The recursion verifier's presets (RECURSION_VERIFIER_PRESETS): same crate dir, one # differently named [[bin]] per preset (recursion--bench, gated on that # preset's Cargo feature) -> a differently named artifact. Generated per preset # from RECURSION_VERIFIER_PRESETS via define/foreach/eval rather than a pattern @@ -236,6 +242,14 @@ $(RECURSION_ARTIFACTS_DIR)/recursion-$(1).elf: FORCE | prepare-sysroot $(RECURSI endef $(foreach preset,$(RECURSION_VERIFIER_PRESETS),$(eval $(call recursion_verifier_rule,$(preset)))) +# Continuation variants: same crate, `continuation` feature on top of the preset +# feature -> recursion-cont--bench -> recursion-cont-.elf. +define recursion_cont_verifier_rule +$(RECURSION_ARTIFACTS_DIR)/recursion-cont-$(1).elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR) + $$(call build_guest_elf,$$(RECURSION_GUESTS_DIR)/recursion,recursion-cont-$(1)-bench,--features "continuation $(1)") +endef +$(foreach preset,$(RECURSION_CONT_PRESETS),$(eval $(call recursion_cont_verifier_rule,$(preset)))) + clean-asm: -rm -rf $(ASM_ARTIFACTS_DIR) @@ -278,6 +292,10 @@ test-profile-recursion-single: compile-recursion-elfs test-profile-recursion-multi: compile-recursion-elfs cargo test --package lambda-vm-prover --lib test_recursion_profile_multiquery -- --ignored --nocapture +# Real-block profile (ethrex, blowup=4/4 transfers), via the `continuation` guest. +test-profile-recursion-block: compile-recursion-elfs $(RUST_ARTIFACTS_DIR)/ethrex.elf + cargo test --package lambda-vm-prover --lib --release test_recursion_profile_blowup4_block -- --ignored --nocapture + # Regenerate the committed ethrex block fixtures (see tooling/ethrex-fixtures). # Run after bumping the ethrex rev; README checksums are refreshed automatically. regen-ethrex-fixtures: diff --git a/bench_vs/lambda/recursion/Cargo.toml b/bench_vs/lambda/recursion/Cargo.toml index 473e3107c..f612949a8 100644 --- a/bench_vs/lambda/recursion/Cargo.toml +++ b/bench_vs/lambda/recursion/Cargo.toml @@ -9,26 +9,57 @@ edition = "2024" # Exactly one selects the fixed ProofOptions (see main.rs) — hardcoded, not # private input, so a malicious input can't downgrade the security level. # Cargo features are additive by design, so the compile_error! mutual-exclusion -# guard in main.rs is the loud failure that stops a mislabeled artifact if both +# guard in main.rs is the loud failure that stops a mislabeled artifact if two # ever get enabled at once (e.g. under `--all-features`). The crate must stay a # standalone `[workspace]` (not a root-workspace member) so root-level feature -# unification can never turn both on. +# unification can never turn two on. min = [] +blowup2 = [] +blowup4 = [] blowup8 = [] +# Orthogonal to the presets: verify a ContinuationProof bundle (multi-epoch, +# memory-bounded inner prove) instead of a monolithic VmProof. Selects the +# `recursion-cont--bench` bins below. +continuation = [] # One distinctly named binary per preset (selected by its feature) so a parallel # `make -j` builds them to different filenames — structurally race-free, no cp -# clobbering. Both use src/main.rs; required-features gates each to its preset. +# clobbering. All use src/main.rs; required-features gates each to its preset. [[bin]] name = "recursion-min-bench" path = "src/main.rs" required-features = ["min"] +[[bin]] +name = "recursion-blowup2-bench" +path = "src/main.rs" +required-features = ["blowup2"] + +[[bin]] +name = "recursion-blowup4-bench" +path = "src/main.rs" +required-features = ["blowup4"] + [[bin]] name = "recursion-blowup8-bench" path = "src/main.rs" required-features = ["blowup8"] +[[bin]] +name = "recursion-cont-min-bench" +path = "src/main.rs" +required-features = ["continuation", "min"] + +[[bin]] +name = "recursion-cont-blowup2-bench" +path = "src/main.rs" +required-features = ["continuation", "blowup2"] + +[[bin]] +name = "recursion-cont-blowup4-bench" +path = "src/main.rs" +required-features = ["continuation", "blowup4"] + [dependencies] lambda-vm-prover = { path = "../../../prover", default-features = false, features = [ "profile-markers", diff --git a/bench_vs/lambda/recursion/src/main.rs b/bench_vs/lambda/recursion/src/main.rs index 33a061364..9e190ad14 100644 --- a/bench_vs/lambda/recursion/src/main.rs +++ b/bench_vs/lambda/recursion/src/main.rs @@ -12,15 +12,23 @@ //! `recursion::verify_and_attest_blob` — no deserialization pass, no owned //! `VmProof`. //! -//! `ProofOptions` is fixed by the `min`/`blowup8` Cargo feature (a `Preset`), -//! not private input — an attacker could otherwise pick trivially weak options -//! and have the guest accept as if a real proof had been checked. +//! The `continuation` feature swaps the monolithic proof for a multi-epoch +//! `ContinuationProof` bundle on the same wire format +//! (`recursion::ContinuationGuestInput`, built by +//! `recursion::encode_continuation_guest_input`), verified via +//! `recursion::verify_continuation_and_attest_blob` — same trust model; the +//! bundle is materialized with one rkyv deserialize pass (zero-copy epoch +//! verify is follow-up work). //! -//! On success commits `program_id || inner_public_output` via -//! `recursion::verify_and_attest_blob` (a single ELF parse and a single -//! full-ELF Keccak, shared between the statement absorb and the `program_id` -//! fold). The id fold is what the consumer rebinds to a trusted ELF -//! (`check_attestation`); it is not self-enforcing here — the binding is +//! `ProofOptions` is fixed by exactly one preset Cargo feature +//! (`min`/`blowup2`/`blowup4`/`blowup8` — a `Preset`), not private input — an +//! attacker could otherwise pick trivially weak options and have the guest +//! accept as if a real proof had been checked. +//! +//! On success commits `program_id || inner_public_output` (a single ELF parse +//! and a single full-ELF Keccak, shared between the statement absorb and the +//! `program_id` fold). The id fold is what the consumer rebinds to a trusted +//! ELF (`check_attestation`); it is not self-enforcing here — the binding is //! established by the consumer via `recursion::check_attestation` (a //! host-side recompute+compare), never in-guest. //! @@ -31,14 +39,30 @@ use lambda_vm_prover::recursion::Preset; -#[cfg(not(any(feature = "min", feature = "blowup8")))] -compile_error!("select exactly one of the `min`/`blowup8` features"); -#[cfg(all(feature = "min", feature = "blowup8"))] -compile_error!("select exactly one of the `min`/`blowup8` features"); +#[cfg(not(any( + feature = "min", + feature = "blowup2", + feature = "blowup4", + feature = "blowup8" +)))] +compile_error!("select exactly one of the `min`/`blowup2`/`blowup4`/`blowup8` features"); +#[cfg(any( + all(feature = "min", feature = "blowup2"), + all(feature = "min", feature = "blowup4"), + all(feature = "min", feature = "blowup8"), + all(feature = "blowup2", feature = "blowup4"), + all(feature = "blowup2", feature = "blowup8"), + all(feature = "blowup4", feature = "blowup8"), +))] +compile_error!("select exactly one of the `min`/`blowup2`/`blowup4`/`blowup8` features"); /// The build preset fixing the inner `ProofOptions` (see the module docs). #[cfg(feature = "min")] const PRESET: Preset = Preset::Min; +#[cfg(feature = "blowup2")] +const PRESET: Preset = Preset::Blowup2; +#[cfg(feature = "blowup4")] +const PRESET: Preset = Preset::Blowup4; #[cfg(feature = "blowup8")] const PRESET: Preset = Preset::Blowup8; @@ -65,9 +89,18 @@ pub fn main() -> ! { // is what the consumer rebinds to a trusted ELF (`check_attestation`); it is // not self-enforcing here. let options = PRESET.options(); + + #[cfg(not(feature = "continuation"))] let attestation = lambda_vm_prover::recursion::verify_and_attest_blob(blob, &options) .expect("verify errored") .expect("inner proof failed verification"); + + #[cfg(feature = "continuation")] + let attestation = + lambda_vm_prover::recursion::verify_continuation_and_attest_blob(blob, &options) + .expect("verify errored") + .expect("inner continuation proof failed verification"); + lambda_vm_syscalls::syscalls::commit(&attestation); lambda_vm_syscalls::syscalls::sys_halt(); } diff --git a/executor/.gitignore b/executor/.gitignore index fa48867ab..3ed575591 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -2,3 +2,9 @@ /program_artifacts/rust /tests/ethrex_hoodi.bin /tests/ethrex_bench_*.bin +# The small scaling-ladder fixtures ARE committed (scripts/bench_recursion_scaling.sh +# reads them; ~13-30 KB each). Bigger generated fixtures (e.g. _20) stay ignored. +!/tests/ethrex_bench_1.bin +!/tests/ethrex_bench_4.bin +!/tests/ethrex_bench_8.bin +!/tests/ethrex_bench_16.bin diff --git a/executor/tests/ethrex_bench_1.bin b/executor/tests/ethrex_bench_1.bin new file mode 100644 index 0000000000000000000000000000000000000000..6a9c94380a13bb2d863e56b2e1cb3e75af75cb01 GIT binary patch literal 13341 zcmeHO3tUWF+h1$X{aUG%O65>VD7w0xN|aQ(m@bT(-A$1s5<+IW=zlziT_Lx~e0wCv=kU(FZ~c7E?|b*}H+w&8?Pvd=XRZIUp1ta9WOqE#q z&7+RqS6+CW?P_kV;HQ)OzQev~I}P_<{#osWC7coSW*=VoX4Cs1203F?+@x#gs({N}z{!L7Qj(us+Ko!~H9>ZHG#yZu_7l zlls2vLP|j42#s9RyIyID)>%m@iYpzCJAT3Z>YDG1ZXW~!q4wy{?SsQV3%bj&QT6Ti zQ3kbfMpX@D&=JlYP7j--&y__jojrhxfJfx>STgA3PxZnI$l^Gls+^Z>p`x&2Dsd{$ zk)zN5bkY(0$bh-f01ln%%+W`o8IC7~iS8J_8Sb;hCw`60(Z@~-6^TEFtaL`DI4thE zZ=3UNS;if9IioNPK_Q>R=7dSfqQ1^N$PY&%!Vx}xGYa8mK5FB{<|p#;33hgN2B?t? z8trT#yiQNJ``lzb$zCKDb_EI@Q5=hl;t&)Mb$O0h6-WQm0aexKJL;mSB6gJ#2bD2I ztvQ*(D{zj0ja`ExEN&Q%i%LqN&CYhHB%8zL@h~65ox^yTk`RxFS|dCb2ElVcWv~*2 zEKE{`d=!D-Y4Eo(FgVPCk4j?NCQ5uBYNW(NNpLm~)xiX!P#B2`1figc33#C(j|n)T zAc+ZxP#|LhgtKQ09dB`BZ-(3i%*P~W%mqhxn2B|k9S0LRSgY7#ks#|1FFV_D*eJw$ zAS?_@xx-dxJ3Ed$eBi`}D8k3FIT#$)t=33yc>O4pYQ^V0_21+j{mgr+koVNDc^4vm z%{%^ky!Y~N@)!T;C;o%J%ReH8-pBYy`uNxU#Q!H;9vbvr{)OE1MFI}8_zpsL9)4kG z(r0$e|08ym^|1qc*@^CDCr-!?{EMBS&+Ht1FA%t6ktczrn%UTKE_Qs2<(!!x1%hS}3 zk1|CiXORkp!sPk?dxP8va_3bpH)6hzffP4WRA)r@Dh$$#n3P%pM0PM&G-9Tx+hIC( zPxC+kAX1rB0vXZ}1Q19RDng@B$dH0iC=3P;p_AwcnTp5MAel+00UCoz!BHguAW<1) z3Ka(>-%f*Ek^W|`JYVU6_&SU%s(hcKr8G-wQD|9*R4Y3!A#l+Lk4=pz5`x`!jImBQ zj2(r2@h5gP@9mTy?4igY`$-z$ygUBsV9w)Cr}yzbLmqb8Rs}h&C`Xd(oSR4%B_3Wff&c+gs(zz)b z`zaA?j_>}{f%Ggg{=xa~367-^a~~k-fP_$IIgRvZ}pGqtr(ew?E!7 z-Ef&WR=hv>PiK$0TuP)K8GCKntxI=CZ+C1~3qMos+FC!s#lsH*RNwt>yPBbFx!HbP zcF4&#zs{F+JFcuIm3CdG86GT~4+ddL?gy;)Rzr}Idd32^KagKZS&U3~+A%q9Oq8{j z88_p_95vFTN<*e-Pw(_iPV4)Ge`)gTlSBfON~IulB8`rxLmZXBq~i$$gd_pz*f5}y zDL8~cCX)~{iAZ76NmM3Xf=OghByb25kBB)UUWHsXQQNi@^$#8r+UfAJ)-n24^XBxH z&bG=lkJ_$7qmk9v2>2cI>K`~FewC5=bNm2`wV?{*ADkZq#)L;ca5^1PR3u|ta7udW z>Ct`x*8VFwqGMHz5itO5>BoqGKud#V^^@jW59zX6 z5X=h`PENesm~QlSrMW#F)p_>lsO>ZjSLCTprFTHN+_F7? zw`v|VrKkC2&zoDxEp1`9fGm$P*%cM{+-5;w8`GBa{Gc3TPtcGT61P6eYCf^Zxw*%- zi1+$bOZKTv()|c6I*Ep5P%^|OAxs*D%%IRoWCjyYg%S*s1cO4r5hUo?p2!S zR4Nq@NkkG3B2>w@v$^pl$yZiiB*`AJ%8(Q$anX~JvKn)^{>|{o388EAZkOXq?t!DT zE5bA;t?s^oca6+ig7{;pcM1me^;(*v5c|j5rO)%0A8LrfgJ`w~LIaqu$}_JTwq~U9 zK7}2cj*YSY&ekMMxir){4gHPiO(-`=c~ZRWVg7Peb$H zGb^fJMaj>WX1@lGbMlff|2&+^XP4?mXrgw`%j?qIc=+PTYH&7m#m28Of51u2AABkhNBKIfH zF}$q#9Cvl$Si&4o7LhXiuxX1KrJ+s0RTBf_R|y;-R~mk<#<0WTd8{HQ%By4bsu{UX&CGZc>WbTpjaxlt zg2Qd2d@hzFD!TP1w95n07SR~}7!eR0FW$2%Hdl(wDMRt&n6)-#!Q%w>AG}Jwf2{Aol*c9uh%^S=jPPW|dyW+NchvoQTZQ@|;L!MSsukWiM z2+Y#5Srcr!cg%}+TykthxXh`C$(@M17#P1wq7>~1Fxc$NZ1(O-TecCkIz9Ntl)|YC z+}hYiw&lijuUHP2|7zAQrSu_W_$#TFI2JZ`ceH_ogd zp+%w*X*dFzLdP)y6W}GNbU>lei8wq12M_{Df=s92C2#~h1waWRLIw<~1VX}*0F?nC zLGrB)H-_L>?l#5~dwv>#r>CW#7q0exzU^mXN85jyiNpvva3iMrbxxj3ZP@GQMur8` z{G|)k1}R^v_Tl%u){Tz4I9m)1(ZTXlG{jk5Ft~X4B{@0oS?Yq6b=~BwlHGacn!65O zxMF+6lQpfVlK_F&0a|$M@R_sM=?ZhPu`^^c6rQvwv9x@TMM)TQnmDj~WF5Ev-v6x$a0g~zTiKAdc$ ze|y@&g|lQ9u1Y^x6ds*4M8h9~cNTl`i7z!SRCd&NB~4@;@-#`-pQ^sE{n4hn-!AG* zs}aMB=l}*ryl&aJXRpf5S~b-D%{A#WhS)>}8)=^S0Ky-}-cD5?7%N_O7g}#MPH)qN@}}mHCuDUKZJ=5GTfig&1q1ps?ekMhUvP+jPv>y}!qlJfT@+gSyzZ zxii~>ZIq;a-$0Ny|G~3As=J~FTV>m1t$lm!(b*W|nOY9dqeIWyugu(`+Kj~hT@hYtmK4lgQlx1T;!=f#d7XUIvhaXQ&5OoYh3d4E ziSZ4l+XuoyF))Dspizy-3?kp#8^5t?+PsxDcYn#qxrd4ktW!e0t0We#5eFmR??bt^ zrs}r*jIiVpODEU}U}FL_}QQ2`0Z4Tp_~!z;6F$MXnq_~pA&S{ zWYf_Fb6gD>Gj!_CH)gCN`4m*0lc5J%-4dfMU^&sCTsmgxE?d0>HJVdq#u1C{Eu`f; z#y*WSI-XJE-3CE@Lemp1&);O`RxU~^qC`n5&5KahYm=XDP;9p@Y3n>2F&@l;a>MD) zg0@xLs#FZ_nv~4GKEqhPdvETBmurU2XM2oGDHp?2k%^@rp#{PlMSkg0t0Wh=M^8IO zgSzidR+3gJZxnIxbI*E;#wO=F+2Hsi5bV8XA6)U|&nUMN?I*h!PjLLYMOhY*tCw_<%%ke*%T_(CRd zmYTFZzDj**Sn0zf4oXwA1Ykv%NGl{pXz}KeIfpjtFrvzjLjjo(6_hn`Rz}$1v26!b zc4D7yH+JFIoZ$NDj5hu-F*-2)#JS_$npOTEXdc(3BdAm4hd?m$m{sCq@2zdp>&N9k zFLNrBna<5GIu(1hWT!4ZS*6Md%zG!&kMb9fG_PKNJkO?Pnp1%8PCb2BBh#KLw8lJS zD>-HUh|%KT5YJud{owpjoj`!c(x=yr-f~}T_Wfg|ul!Yp{ZjRx315)@e)*SEUxdHw z{JrLjazBMH$bC5QY3KM)G7;Mg|3Blk!1_~}g6)NM-^}=+>}%eC54^9r{{t;ddaeKf literal 0 HcmV?d00001 diff --git a/executor/tests/ethrex_bench_16.bin b/executor/tests/ethrex_bench_16.bin new file mode 100644 index 0000000000000000000000000000000000000000..d466f9aba9ffa1bda3210b8fc262e639d9b5e51f GIT binary patch literal 29082 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

a8|vtSXrVkD`ZC!#k3}*8fZW1P|vhc!0)% z@nLj$9ED9{zrc*nH0f9|6Gp+&}8+0WqXY%EhbK7oT~8o8@(mnzd3 z>{K*r5|DV3wl^iSzG|+!#?xPBJiuGc(AcHT!|$9M~Ueq`|l1R!Y*))C;j2)fSHb3d;k5(0m@vq{s54*?5cW2Qqx3lhjj- zWS#BG`1Za$Y6u8W>ofAWezv_&;3|!SQF^MkwD#N061Q{Pwm;h^AhP#R2Lv=v+fCox zIP_dm!?l-@~>pm-XbtGYf^UMB9&hOoHuYei>`14{ZRoUzHTb@r?7 z6C#8`N{GcS$ZGg7G*=M*cCym&-M?+ft>a8f2IYa(W;3f@^rXdXUn3yS`ceHKmtL;# zsgta?)8oz2CwUt*W-1y!4_KQw+chy>rVFA)pg4&l2+D*Z&?JfAOcs+45BAbYCXEiD z#|fU+!gw&Qm`)?1>z@s91Yy!)cw1Xk908+=f03AI1SAz~y4KHWhlcUls?AFNvCHmV zkTt42>S&SkdRb{7q@@f2cG9;kuDtPT5L3uP&)j=*)hLYE1qM22HqXo+qT#Rz3TF7+ zldcE7ej7I~lK<4ZMBV?7Q6YWTi)WYwuAhH6`l>O+{(aNMpQY3kU|@F`_$70&Ml-t# zr|hw`X!hvPMzMwJ){m9mZt>&HP*h7i@q)S^LQnwh{gkEm11qWq zs*i3KOS$ztOzxuQ9AWC%zpzJduRK5R__c?Ej9MQ|)e)^7C-?AT#XA|LTiFUR*M+HL zbB(>V=xuR>134}zw|OgGPZKir6Sv)z|bxzH5O0!$wC7{Is-Iupiq!)E|AfYL}vK%mfZ_6tpa zgZrB7H5(ea#9;M0{ee4dRo;;z_mfn%&dvjtvy~fk9t$(hhv|pnkR$6vl{V&D&d$1U z=jg%6xo%YwS}Cp~^1%&{)eta@KhY>LsnmPgZ)TN-hl=B?a&s#N+_Uv`N6x;|S^l6- zm{$57TZ#0Kn*VfA^J`wv%F@y@bb!`<9o4=5uiSI0OcdbJgs6(rDA5^p)fbxsj2y{J zkLGA@(O-0C=Mm>VJ~mTS9hVEy3Z-NUp&`I$?V{W;gE9b4Vgc#(SDhAIRw_9hEP6F% zL=Iw(XPKvh)X_E9yzmf0Gwb?$_3F*e(mD`$4#xSDRvTB{PColp_;hwa}L zjF6X!KAhkkHF){Bl;y{qeYfa|GQye`8V}Ga@)D*WVAZr!y_YJ{m{=WBj|)1c(zZU=AISGKNHUK1g%Yojv_JM&=uATfbRIRgIh zNy#{tz>~Ip;J~hac=PS_-jy+9lGSIi(us=ID_3a?(MmT6-`^T_&qfiHVDn(e4%}~( zfPu2<92g49gYODrG2zyN2VXOU!Q;y)lgouSY4B}aTmlB^V;lte&zv@cs|?zu3e^AZ z)Q45x1;_2VZhCvoQZ9)4T|XmszAz*#@}FV!zo(UMdBavWs0Q_jb>*qs9UTOUni4M$ zUJS*=#{2Cb97OEjKhRDfabnqWkJr)Rezv3x0vhYLzjjN|oOrQzpB`>D#ZUdXn7#JY z?5aVMK~g;>he4w?IV<(5yY5P}GySc$d+AS_{Gl1U?iR#t*0M|8=eNXF838vmSkJ8+ zlq8?C%EyJeznuTR&PzMB$$7m`QCeoqBxzx69@J>T!OK10md2ej9d_ooV9mJ^CfbW? zLYqcZZ{OOw-gsrst5XHN7v(&ZTXb9WLdNKa6Ig}2)>Y0Z2Sc#_hV$cp#rDRW z8T0s}Mt|DGAz4A5fht*RkBNt_pZt4ezbjS?Bnl^3AYgLO;JB5Uk?+l;oD);x1-B*Zip-+Ue7jb|G5shvjjN&*U(9ap8!a}f9}Uz_k0Y9&6uRRB->}}3C75Ys~B(L Q&orX(^e4Fg2ZhG>g#Z8m literal 0 HcmV?d00001 diff --git a/executor/tests/ethrex_bench_4.bin b/executor/tests/ethrex_bench_4.bin new file mode 100644 index 0000000000000000000000000000000000000000..45fe930380e9f02aedaa1aeb681926704d058346 GIT binary patch literal 17371 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 IlE0YyFQDcqaR2}S literal 0 HcmV?d00001 diff --git a/executor/tests/ethrex_bench_8.bin b/executor/tests/ethrex_bench_8.bin new file mode 100644 index 0000000000000000000000000000000000000000..b5790e9492ee904614ed07b23ff6ca560846206d GIT binary patch literal 21410 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 Rq8}Qd3;O=1TghMG{txSn0v`YX literal 0 HcmV?d00001 diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 654b58fa4..2ac9fcfc0 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -20,9 +20,9 @@ //! //! [`program_id`] deliberately does not fold the `ProofOptions`: the security //! level is pinned by which verifier guest the outer proof is checked against -//! (`recursion-min.elf` vs `recursion-blowup8.elf`, fixed at build time — see -//! [`Preset`]). A consumer must pin that outer ELF too, or a 1-query `min` -//! attestation is indistinguishable from a 128-bit `blowup8` one. +//! (`recursion-min.elf` vs `recursion-blowup2.elf`/`recursion-blowup8.elf`, +//! fixed at build time — see [`Preset`]). A consumer must pin that outer ELF +//! too, or a 1-query `min` attestation is indistinguishable from a 128-bit one. use crypto::hash::platform_keccak::PlatformKeccak256 as Keccak256; use digest::Digest; @@ -52,15 +52,37 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { pub enum Preset { /// Blowup=2, 1 query ([`MIN_PROOF_OPTIONS`]) — insecure, diagnostics only. Min, - /// Blowup=8, multi-query — 128-bit security. + /// Blowup=2, full 128-bit query count (219 queries at 20 grinding bits) — + /// the realistic base-layer shape: production pipelines prove the base + /// proof at low blowup (2/4) and reserve high blowup for the final wrap. + Blowup2, + /// Blowup=4, 110 queries — the other realistic base-layer point (e.g. + /// Zisk's compressor layer): 2× the prover LDE of blowup=2 for half the + /// queries to verify. + Blowup4, + /// Blowup=8, multi-query (73 queries) — 128-bit security at final-wrap-style + /// parameters: more prover work per row, far fewer queries to verify. Blowup8, } impl Preset { + /// Every preset, for name→preset lookups (e.g. the blob-dump test's + /// `RECURSION_DUMP_PRESET`). Keep in sync with the enum. + pub const ALL: [Preset; 4] = [ + Preset::Min, + Preset::Blowup2, + Preset::Blowup4, + Preset::Blowup8, + ]; + /// The fixed `ProofOptions` this preset's guest verifies with. pub fn options(&self) -> ProofOptions { match self { Preset::Min => MIN_PROOF_OPTIONS, + Preset::Blowup2 => crate::GoldilocksCubicProofOptions::with_blowup(2) + .expect("blowup=2 is always valid"), + Preset::Blowup4 => crate::GoldilocksCubicProofOptions::with_blowup(4) + .expect("blowup=4 is always valid"), Preset::Blowup8 => crate::GoldilocksCubicProofOptions::with_blowup(8) .expect("blowup=8 is always valid"), } @@ -71,6 +93,8 @@ impl Preset { pub fn artifact_stem(&self) -> &'static str { match self { Preset::Min => "recursion-min", + Preset::Blowup2 => "recursion-blowup2", + Preset::Blowup4 => "recursion-blowup4", Preset::Blowup8 => "recursion-blowup8", } } @@ -79,6 +103,8 @@ impl Preset { pub fn name(&self) -> &'static str { match self { Preset::Min => "min", + Preset::Blowup2 => "blowup2", + Preset::Blowup4 => "blowup4", Preset::Blowup8 => "blowup8", } } @@ -127,6 +153,49 @@ pub fn encode_guest_input( }) } +/// The continuation guest's private-input layout (the `continuation` guest +/// feature). Mirrors [`crate::GuestInput`] with the monolithic proof replaced +/// by the bundle and the PAGE roots replaced by the global-memory genesis +/// roots (see [`crate::continuation::continuation_precomputed_commitments`]). +/// Rkyv-archived on the same magic-prefixed wire format as the monolithic +/// blob ([`crate::encode_recursion_input`]); the guest is feature-pinned to +/// one layout, and a blob of the other kind fails the bytecheck validation. +#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +pub struct ContinuationGuestInput { + pub bundle: crate::continuation::ContinuationProof, + pub inner_elf: Vec, + pub decode_commitment: Commitment, + pub page_commitments: Vec<(u64, Commitment)>, +} + +/// Build the continuation guest's private-input blob for `bundle` of +/// `inner_elf`: precomputes the roots and rkyv-encodes a +/// [`ContinuationGuestInput`] behind the standard aligning prefix. Takes the +/// bundle by value (it is large; the encoder is its last consumer). +pub fn encode_continuation_guest_input( + bundle: crate::continuation::ContinuationProof, + inner_elf: &[u8], + opts: &ProofOptions, +) -> Result, Error> { + let (decode_commitment, page_commitments) = + crate::continuation::continuation_precomputed_commitments(inner_elf, &bundle, opts)?; + let input = ContinuationGuestInput { + bundle, + inner_elf: inner_elf.to_vec(), + decode_commitment, + page_commitments, + }; + let archive = rkyv::to_bytes::(&input) + .map_err(|e| Error::Execution(format!("rkyv encode failed: {e}")))?; + let mut blob = Vec::with_capacity(crate::RECURSION_INPUT_PREFIX_LEN + archive.len()); + blob.extend_from_slice(&crate::RECURSION_INPUT_MAGIC); + blob.extend_from_slice(&crate::RECURSION_INPUT_VERSION.to_le_bytes()); + blob.extend_from_slice(&[0u8; 4]); // reserved + debug_assert_eq!(blob.len(), crate::RECURSION_INPUT_PREFIX_LEN); + blob.extend_from_slice(&archive); + Ok(blob) +} + /// Domain tag for [`program_id`]. const PROGRAM_ID_TAG: &[u8] = b"LAMBDAVM_PROGRAM_ID_V1"; @@ -223,49 +292,6 @@ pub fn verify_and_attest_blob( Ok(Some(attestation)) } -/// The continuation guest's private-input layout (the `continuation` guest -/// feature). Mirrors [`crate::GuestInput`] with the monolithic proof replaced -/// by the bundle and the PAGE roots replaced by the global-memory genesis -/// roots (see [`crate::continuation::continuation_precomputed_commitments`]). -/// Rkyv-archived on the same magic-prefixed wire format as the monolithic -/// blob ([`crate::encode_recursion_input`]); the guest is feature-pinned to -/// one layout, and a blob of the other kind fails the bytecheck validation. -#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -pub struct ContinuationGuestInput { - pub bundle: crate::continuation::ContinuationProof, - pub inner_elf: Vec, - pub decode_commitment: Commitment, - pub page_commitments: Vec<(u64, Commitment)>, -} - -/// Build the continuation guest's private-input blob for `bundle` of -/// `inner_elf`: precomputes the roots and rkyv-encodes a -/// [`ContinuationGuestInput`] behind the standard aligning prefix. Takes the -/// bundle by value (it is large; the encoder is its last consumer). -pub fn encode_continuation_guest_input( - bundle: crate::continuation::ContinuationProof, - inner_elf: &[u8], - opts: &ProofOptions, -) -> Result, Error> { - let (decode_commitment, page_commitments) = - crate::continuation::continuation_precomputed_commitments(inner_elf, &bundle, opts)?; - let input = ContinuationGuestInput { - bundle, - inner_elf: inner_elf.to_vec(), - decode_commitment, - page_commitments, - }; - let archive = rkyv::to_bytes::(&input) - .map_err(|e| Error::Execution(format!("rkyv encode failed: {e}")))?; - let mut blob = Vec::with_capacity(crate::RECURSION_INPUT_PREFIX_LEN + archive.len()); - blob.extend_from_slice(&crate::RECURSION_INPUT_MAGIC); - blob.extend_from_slice(&crate::RECURSION_INPUT_VERSION.to_le_bytes()); - blob.extend_from_slice(&[0u8; 4]); // reserved - debug_assert_eq!(blob.len(), crate::RECURSION_INPUT_PREFIX_LEN); - blob.extend_from_slice(&archive); - Ok(blob) -} - /// [`verify_and_attest_blob`]'s logic for a continuation bundle: takes the /// wire-format blob ([`encode_continuation_guest_input`]) and does the /// intended `continuation` guest's whole job in one call — verify every diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index bd1244c30..e95b1052a 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -185,6 +185,67 @@ fn setup_guest_run( (guest_elf_bytes, program, executor) } +/// Read the ethrex guest ELF (built by `make executor/program_artifacts/rust/ethrex.elf`). +fn read_ethrex_elf(root: &std::path::Path) -> Vec { + let path = root.join("executor/program_artifacts/rust/ethrex.elf"); + std::fs::read(&path).unwrap_or_else(|e| { + panic!( + "failed to read {} — run `make executor/program_artifacts/rust/ethrex.elf`: {e}", + path.display() + ) + }) +} + +/// Read a committed ethrex block fixture (`executor/tests/ethrex_bench_.bin`). +fn read_ethrex_fixture(root: &std::path::Path, txs: u32) -> Vec { + let path = root.join(format!("executor/tests/ethrex_bench_{txs}.bin")); + std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())) +} + +/// [`setup_guest_run`] for the `continuation` guest feature: proves `inner_elf` +/// on `inner_input` via continuations (`epoch_log2`-cycle epochs) and loads +/// `recursion-cont-.elf` instead of the monolithic `recursion-.elf` +/// — the realistic in-VM cost of verifying a real (multi-epoch) inner proof +/// instead of the `empty`-program diagnostic floor. +fn setup_continuation_guest_run( + label: &str, + preset: Preset, + inner_elf_bytes: &[u8], + inner_input: &[u8], + epoch_log2: u32, +) -> ( + Vec, + executor::elf::Elf, + executor::vm::execution::Executor, +) { + let root = workspace_root(); + let guest_elf_bytes = read_guest_elf(&root, &format!("recursion-cont-{}", preset.name())); + + let opts = preset.options(); + eprintln!( + "[{label}] proving inner continuation (blowup={}, fri_queries={}, epoch=2^{epoch_log2}) ...", + opts.blowup_factor, opts.fri_number_of_queries + ); + let bundle = + crate::continuation::prove_continuation(inner_elf_bytes, inner_input, epoch_log2, &opts) + .expect("inner continuation prove should succeed"); + eprintln!("[{label}] continuation epochs: {}", bundle.num_epochs()); + let blob = recursion::encode_continuation_guest_input(bundle, inner_elf_bytes, &opts) + .expect("recursion::encode_continuation_guest_input failed"); + eprintln!("[{label}] rkyv blob: {} bytes", blob.len()); + + let program = executor::elf::Elf::load(&guest_elf_bytes).expect("ELF load failed"); + assert_ne!( + program.entry_point, + 0, + "recursion-cont-{} ELF has entry_point=0 — build artifact is malformed", + preset.name() + ); + let executor = + executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); + (guest_elf_bytes, program, executor) +} + /// Demangled enclosing-function name for a PC via the ELF symbol table; /// `` if none covers it. No file:line (symtab has no DWARF). fn resolve_pc(symbols: &executor::elf::SymbolTable, pc: u64) -> String { @@ -324,16 +385,68 @@ fn print_step_breakdown(buckets: &[u64; 7], total_cycles: u64) { } } -/// Single-pass execute-only profiler. Always prints total cycles, the -/// per-step cycle breakdown (marker decode is cheap — one `InstructionCache` -/// lookup per cycle), and a rough trace/LDE estimate; with `detailed`, also -/// the top-25 functions table (needs a `pc_hist` HashMap, so gated). +/// Single-pass execute-only profiler over the `empty` inner program (the +/// verifier's intrinsic recursion overhead, not a real workload). Always +/// prints total cycles, the per-step cycle breakdown (marker decode is cheap — +/// one `InstructionCache` lookup per cycle), and a rough trace/LDE estimate; +/// with `detailed`, also the top-25 functions table (needs a `pc_hist` +/// HashMap, so gated). fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { + let (guest_elf_bytes, program, executor) = setup_guest_run("profile", preset); + run_profile_from( + preset, + &guest_elf_bytes, + &program, + executor, + progress_stride, + detailed, + ); +} + +/// [`run_profile`] over a REAL inner program instead of `empty`: verifies +/// `inner_elf_bytes`/`inner_input` via the `continuation` guest +/// (`recursion-cont-.elf`) — the realistic in-VM verifier cost, not +/// the intrinsic-overhead floor. +fn run_profile_continuation( + preset: Preset, + inner_elf_bytes: &[u8], + inner_input: &[u8], + epoch_log2: u32, + progress_stride: usize, + detailed: bool, +) { + let (guest_elf_bytes, program, executor) = setup_continuation_guest_run( + "profile-block", + preset, + inner_elf_bytes, + inner_input, + epoch_log2, + ); + run_profile_from( + preset, + &guest_elf_bytes, + &program, + executor, + progress_stride, + detailed, + ); +} + +/// Shared profiling loop for [`run_profile`]/[`run_profile_continuation`]: runs +/// an already-set-up guest executor and prints the same cycle/step/function +/// breakdown regardless of which inner program it verified. +fn run_profile_from( + preset: Preset, + guest_elf_bytes: &[u8], + program: &executor::elf::Elf, + mut executor: executor::vm::execution::Executor, + progress_stride: usize, + detailed: bool, +) { use std::collections::HashMap; let opts = preset.options(); - let (guest_elf_bytes, program, mut executor) = setup_guest_run("profile", preset); - let symbols = executor::elf::SymbolTable::parse(&guest_elf_bytes); + let symbols = executor::elf::SymbolTable::parse(guest_elf_bytes); let instructions = executor::vm::execution::InstructionCache::new(&program.data) .expect("instruction cache build failed"); @@ -771,19 +884,93 @@ fn test_recursion_prove_1query() { } /// Dump the guest's private-input blob to `/tmp/recursion_input.bin` for the -/// CLI's `execute --flamegraph`. +/// CLI's `execute --flamegraph` and `scripts/bench_recursion_cycles.sh`. +/// +/// Env knobs: +/// * `RECURSION_DUMP_PRESET` (`min`|`blowup2`|`blowup4`|`blowup8`, default +/// `min`) — the [`Preset`] whose options the inner proof is generated under; +/// must match the `recursion-.elf` the blob will be fed to, or the +/// guest rejects the proof. +/// * `RECURSION_DUMP_INNER_ELF` (path, default the `empty` guest) — the inner +/// program to prove, for realistic trace heights (e.g. the ethrex guest). +/// * `RECURSION_DUMP_INNER_INPUT` (path, default none) — the inner program's +/// private input (e.g. an ethrex-fixtures block). +/// * `RECURSION_DUMP_EPOCH_LOG2` (int, default unset = monolithic) — prove the +/// inner via continuations with `2^n`-cycle epochs (memory-bounded prover) +/// and encode a [`recursion::ContinuationGuestInput`] blob instead; feed it +/// to `recursion-cont-.elf`, not the monolithic guest. #[test] #[ignore = "diagnostic: writes recursion private input to /tmp/recursion_input.bin"] fn test_dump_recursion_input() { let root = workspace_root(); - let empty_elf_bytes = read_guest_elf(&root, "empty"); - let (_inner_proof, blob) = - prove_inner_and_encode_blob("dump-input", &empty_elf_bytes, &[], &MIN_PROOF_OPTIONS); + let preset_name = std::env::var("RECURSION_DUMP_PRESET").unwrap_or_else(|_| "min".to_string()); + let preset = Preset::ALL + .into_iter() + .find(|p| p.name() == preset_name) + .unwrap_or_else(|| { + panic!( + "unknown RECURSION_DUMP_PRESET '{preset_name}' (expected min|blowup2|blowup4|blowup8)" + ) + }); + + let (inner_elf_bytes, inner_label) = match std::env::var("RECURSION_DUMP_INNER_ELF") { + Ok(p) => ( + std::fs::read(&p).unwrap_or_else(|e| panic!("read RECURSION_DUMP_INNER_ELF {p}: {e}")), + p, + ), + Err(_) => (read_guest_elf(&root, "empty"), "empty".to_string()), + }; + let inner_input = match std::env::var("RECURSION_DUMP_INNER_INPUT") { + Ok(p) => { + std::fs::read(&p).unwrap_or_else(|e| panic!("read RECURSION_DUMP_INNER_INPUT {p}: {e}")) + } + Err(_) => Vec::new(), + }; + + let blob = match std::env::var("RECURSION_DUMP_EPOCH_LOG2") { + Ok(s) => { + let epoch_log2: u32 = s + .parse() + .unwrap_or_else(|e| panic!("bad RECURSION_DUMP_EPOCH_LOG2 '{s}': {e}")); + let opts = preset.options(); + eprintln!( + "[dump-input] proving inner continuation (blowup={}, fri_queries={}, epoch=2^{epoch_log2}) ...", + opts.blowup_factor, opts.fri_number_of_queries + ); + let bundle = crate::continuation::prove_continuation( + &inner_elf_bytes, + &inner_input, + epoch_log2, + &opts, + ) + .expect("inner continuation prove should succeed"); + eprintln!("[dump-input] continuation epochs: {}", bundle.num_epochs()); + recursion::encode_continuation_guest_input(bundle, &inner_elf_bytes, &opts) + .expect("recursion::encode_continuation_guest_input failed") + } + Err(_) => { + let (_inner_proof, blob) = prove_inner_and_encode_blob( + "dump-input", + &inner_elf_bytes, + &inner_input, + &preset.options(), + ); + blob + } + }; + assert!( + blob.len() <= executor::vm::memory::MAX_PRIVATE_INPUT_SIZE as usize, + "recursion input exceeds MAX_PRIVATE_INPUT_SIZE" + ); let path = "/tmp/recursion_input.bin"; std::fs::write(path, &blob).expect("write blob"); - eprintln!("[dump-input] wrote {} bytes to {path}", blob.len()); + eprintln!( + "[dump-input] preset={} inner={inner_label} wrote {} bytes to {path}", + preset.name(), + blob.len() + ); } /// Cycle count only of the recursion guest verifying a 1-query inner proof. @@ -800,6 +987,49 @@ fn test_recursion_cycles_multiquery() { run_profile(Preset::Blowup8, 500, false); } +/// Cycle count only at 128-bit security with the realistic base-layer shape: +/// blowup=2 yields ~0.49 bits/query, so the full 219-query FRI dominates. +#[test] +#[ignore = "diagnostic: recursion guest cycle count (blowup=2, 219 queries)"] +fn test_recursion_cycles_blowup2() { + run_profile(Preset::Blowup2, 500, false); +} + +/// Cycle count only at 128-bit security, blowup=4 (110 queries) — the other +/// realistic base-layer point. +#[test] +#[ignore = "diagnostic: recursion guest cycle count (blowup=4, 110 queries)"] +fn test_recursion_cycles_blowup4() { + run_profile(Preset::Blowup4, 500, false); +} + +/// Continuation epoch size for the real-block profile below — matches +/// `scripts/bench_recursion_scaling.sh`'s default. +const BLOCK_EPOCH_LOG2: u32 = 21; + +/// Full profile (top-25 + per-step) of the recursion `continuation` guest +/// verifying a REAL ethrex block (4 transfers) — not the `empty`-program +/// diagnostic floor `test_recursion_profile_1query`/`_multiquery` measure. +/// blowup=4 (110 queries): the cheaper of the two realistic base-layer +/// presets, so the full histogram stays tractable; `bench_recursion_cycles.sh`/ +/// `bench_recursion_scaling.sh` already cover cycle counts across every preset +/// and block size, so this only needs to exist for the detailed breakdown. +#[test] +#[ignore = "diagnostic: heavy; recursion guest histogram + steps over a real ethrex block (blowup=4)"] +fn test_recursion_profile_blowup4_block() { + let root = workspace_root(); + let ethrex_elf_bytes = read_ethrex_elf(&root); + let fixture = read_ethrex_fixture(&root, 4); + run_profile_continuation( + Preset::Blowup4, + ðrex_elf_bytes, + &fixture, + BLOCK_EPOCH_LOG2, + 500, + true, + ); +} + /// Full profile (top-25 + per-step) of the 1-query run. #[test] #[ignore = "diagnostic: ~8 min; recursion guest histogram + steps (1 query)"] diff --git a/scripts/bench_recursion_cycles.sh b/scripts/bench_recursion_cycles.sh index 5db264a45..c6ae76058 100755 --- a/scripts/bench_recursion_cycles.sh +++ b/scripts/bench_recursion_cycles.sh @@ -18,8 +18,8 @@ # single measuring CLI (MEASURE_CLI) built once from the checkout this script runs in: # * Guest cycles — retired instructions. # * Keccak calls — keccak-permutation accelerator ecalls (one cycle each, but each -# runs a whole permutation invisibly, so it's the companion signal; -# currently 0 until the verifier is wired to the keccak syscall). +# runs a whole permutation invisibly, so it's the companion signal: +# the verifier's Merkle/transcript hashing rides on this syscall). # The CLI also prints an Ecsm (EC scalar-mul) count, but the STARK verifier does no # scalar-mul, so it is structurally 0 for a recursion proof — dropped as noise, not read. # MEASURE_CLI's executor counts ANY ref's guest ELF correctly (it just feeds the blob @@ -35,14 +35,22 @@ # Usage: scripts/bench_recursion_cycles.sh REF_A [REF_B=origin/main] [PRESET=min] # REF_A ref/SHA to evaluate (the PR side). # REF_B baseline ref/SHA (default origin/main). -# PRESET recursion-verifier preset (default min). Per ref the tool prefers -# recursion-.elf and falls back to recursion.elf (older refs / main -# build a single unnamed recursion guest). It is EXPECTED and correct for the -# two sides to use DIFFERENT artifacts — e.g. main→recursion.elf while a -# preset PR→recursion-min.elf — because both are verified under the SAME min -# proof options (the dump test pins MIN_PROOF_OPTIONS). The printed per-ref -# `guest=` labels show which each side used; a differing name is the -# expected comparison, not a mismatch. +# PRESET recursion-verifier preset (default min): min = blowup=2, 1 query +# (cheap diagnostic regime); blowup2 = blowup=2, 219 queries (the +# realistic base-layer proof shape at 128-bit — production pipelines +# prove the base at low blowup and wrap at high blowup); blowup4 = +# blowup=4, 110 queries (the other realistic base-layer point); +# blowup8 = blowup=8, 73 queries. The preset picks BOTH the guest ELF +# (recursion-.elf, falling back to recursion.elf on older refs +# that build a single unnamed min guest) AND the inner-proof options of +# the dumped blob (exported as RECURSION_DUMP_PRESET to the dump test). +# Refs predating the preset-aware dump test always dump min options, so +# only PRESET=min is measurable for them — the script fails loudly +# up front rather than let the guest reject the blob in-VM. It is +# EXPECTED and correct for the two sides to use DIFFERENT artifact +# names (e.g. main→recursion.elf vs PR→recursion-min.elf) — both are +# verified under the SAME preset options. The printed per-ref +# `guest=` labels show which each side used. # Env: # REBUILD=1 force rebuild of MEASURE_CLI and re-run of every ref # (guest build + blob dump + measurement); ignore caches. @@ -104,8 +112,8 @@ prune_worktree_cache() { echo "==> Pruning old ref worktree $wt (keeping newest $PRUNE_KEEP)" >&2 git worktree remove --force "$wt" >/dev/null 2>&1 || rm -rf "$wt" rm -f "$WORK"/result_"${s8}"_*.txt "$WORK"/blob_"${s8}"_*.bin \ - "$WORK"/build_guest_"${s8}".log "$WORK"/dump_"${s8}".log \ - "$WORK"/measure_"${s8}".err + "$WORK"/build_guest_"${s8}".log "$WORK"/dump_"${s8}"*.log \ + "$WORK"/measure_"${s8}"*.err done <<< "$stale" git worktree prune >/dev/null 2>&1 || true } @@ -229,22 +237,29 @@ measure_ref() { fi echo "==> [$role] guest ELF: $(basename "$guest_elf")" >&2 - # 2c. Generate this ref's own input blob via its ignored dump test. + # 2c. Generate this ref's own input blob via its ignored dump test, under this + # preset's options (RECURSION_DUMP_PRESET). Refs predating the preset-aware dump + # test always prove the min options; feeding that blob to a non-min guest would + # only fail in-VM verification much later, so refuse loudly up front instead. if ! grep -rq "fn test_dump_recursion_input" "$wt/prover/src/tests/" 2>/dev/null; then echo "ERROR: [$role] ref $ref ($sha8) has no 'test_dump_recursion_input' — cannot generate its input blob." >&2 exit 1 fi - echo "==> [$role] dumping recursion input blob (cargo test test_dump_recursion_input) ..." >&2 + if [ "$PRESET" != "min" ] && ! grep -rq "RECURSION_DUMP_PRESET" "$wt/prover/src/tests/" 2>/dev/null; then + echo "ERROR: [$role] ref $ref ($sha8) predates the preset-aware dump test (no RECURSION_DUMP_PRESET) — only PRESET=min is measurable for it." >&2 + exit 1 + fi + echo "==> [$role] dumping recursion input blob (cargo test test_dump_recursion_input, preset=$PRESET) ..." >&2 rm -f /tmp/recursion_input.bin - local dlog="$WORK/dump_${sha8}.log" + local dlog="$WORK/dump_${sha8}_${PRESET}.log" if [ -n "${HOST_TARGET_DIR:-}" ]; then - if ! ( cd "$wt" && CARGO_TARGET_DIR="$HOST_TARGET_DIR" cargo test -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then + if ! ( cd "$wt" && RECURSION_DUMP_PRESET="$PRESET" CARGO_TARGET_DIR="$HOST_TARGET_DIR" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 tail -40 "$dlog" >&2 exit 1 fi else - if ! ( cd "$wt" && cargo test -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then + if ! ( cd "$wt" && RECURSION_DUMP_PRESET="$PRESET" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 tail -40 "$dlog" >&2 exit 1 @@ -262,9 +277,9 @@ measure_ref() { echo "==> [$role] measuring: $MEASURE_CLI execute $(basename "$guest_elf") --private-input --cycles" >&2 local t0 t1 dt out t0=$(date +%s) - if ! out="$("$MEASURE_CLI" execute "$guest_elf" --private-input "$blob" --cycles 2>"$WORK/measure_${sha8}.err")"; then + if ! out="$("$MEASURE_CLI" execute "$guest_elf" --private-input "$blob" --cycles 2>"$WORK/measure_${sha8}_${PRESET}.err")"; then echo "ERROR: [$role] MEASURE_CLI execute failed for $ref ($sha8). Tail of stderr:" >&2 - tail -20 "$WORK/measure_${sha8}.err" >&2 + tail -20 "$WORK/measure_${sha8}_${PRESET}.err" >&2 exit 1 fi t1=$(date +%s); dt=$((t1 - t0)) @@ -334,10 +349,13 @@ mcycd() { } # Human label for the proof regime this preset measures, so a reader can't mistake the -# single-query `min` number for the full 128-bit verifier cost. CI always passes `min`. +# single-query `min` number for the full 128-bit verifier cost. CI passes `min` plus +# the full-query regimes `blowup2`/`blowup4` (see .github/workflows/bench-verify.yml). case "$PRESET" in min) REGIME="single query (blowup=2, 1 query)" ;; - blowup8) REGIME="128-bit (blowup=8, multi-query)" ;; + blowup2) REGIME="128-bit (blowup=2, 219 queries — realistic base-layer)" ;; + blowup4) REGIME="128-bit (blowup=4, 110 queries — realistic base-layer)" ;; + blowup8) REGIME="128-bit (blowup=8, 73 queries)" ;; *) REGIME="$PRESET" ;; esac diff --git a/scripts/bench_recursion_scaling.sh b/scripts/bench_recursion_scaling.sh new file mode 100755 index 000000000..3242c0b06 --- /dev/null +++ b/scripts/bench_recursion_scaling.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# bench_recursion_scaling.sh — in-VM recursion-verifier scaling ladder. +# +# Sweeps ethrex block sizes × verifier presets: proves each block's inner +# execution via CONTINUATIONS (memory-bounded 2^EPOCH_LOG2-cycle epochs, so any +# block size proves on a bounded-RAM box), then executes the continuation +# recursion guest (recursion-cont-.elf) on the bundle and records the +# EXACT deterministic guest cycle count — the in-VM cost of verifying that +# block's proof. +# +# Sweep order is PRESET-MAJOR on purpose: the full block-size curve for the +# first preset completes before the next preset starts, so the headline regime +# (blowup2 = the realistic base-layer options, 219 FRI queries) yields a usable +# scaling curve as early as possible instead of only when everything ends. +# +# Usage: scripts/bench_recursion_scaling.sh [RESULTS_FILE=/tmp/recursion_scaling.txt] +# Env: +# TXS="1 4 8 16" block sizes (transfers); fixtures are read from +# executor/tests/ethrex_bench_.bin (committed for +# 1/4/8/16) and generated via tooling/ethrex-fixtures +# when missing. +# PRESETS="blowup2 blowup4 min" verifier presets, most important first. +# EPOCH_LOG2=21 inner continuation epoch size (log2 cycles). +# DUMP_FEATURES="" extra cargo features for the proving dump, +# e.g. DUMP_FEATURES=cuda on a GPU box. +# +# Prereqs (the script fails fast on each): +# cargo build --release -p cli +# make compile-recursion-elfs (recursion-cont-*.elf) +# make executor/program_artifacts/rust/ethrex.elf (the inner guest) +# +# Output: one key=value line per cell in RESULTS_FILE, e.g. +# txs=4 preset=blowup2 epochs=2 blob=145080513 cycles=18984803380 keccak=3152604 exec_wall_s=164 +# Cycle counts are deterministic (machine-independent); wall times are not. +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +TXS="${TXS:-1 4 8 16}" +PRESETS="${PRESETS:-blowup2 blowup4 min}" +EPOCH_LOG2="${EPOCH_LOG2:-21}" +RESULTS="${1:-/tmp/recursion_scaling.txt}" +WORK="$(mktemp -d /tmp/recursion_scaling.XXXXXX)" + +CLI=target/release/cli +ART=executor/program_artifacts/recursion +ETHREX=executor/program_artifacts/rust/ethrex.elf + +[ -x "$CLI" ] || { echo "ERROR: $CLI missing — run: cargo build --release -p cli" >&2; exit 1; } +[ -f "$ETHREX" ] || { echo "ERROR: $ETHREX missing — run: make $ETHREX" >&2; exit 1; } +for P in $PRESETS; do + [ -f "$ART/recursion-cont-${P}.elf" ] || { + echo "ERROR: $ART/recursion-cont-${P}.elf missing — run: make compile-recursion-elfs" >&2 + exit 1 + } +done + +echo "==> results -> $RESULTS (work dir: $WORK)" +: > "$RESULTS" + +for P in $PRESETS; do + for N in $TXS; do + FIX=executor/tests/ethrex_bench_${N}.bin + if [ ! -f "$FIX" ]; then + echo "==> [${P}/${N}tx] generating missing fixture $FIX" >&2 + ( cd tooling/ethrex-fixtures && cargo build --release ) >"$WORK/fixtures_build.log" 2>&1 + tooling/ethrex-fixtures/target/release/ethrex-fixtures "$N" "$FIX" distinct >&2 + fi + + # Inner block cost, once per block size (cheap; repeated per preset is fine — + # the count is deterministic and the run takes well under a second). + ic="$("$CLI" execute "$ETHREX" --private-input "$FIX" --cycles | awk -F': ' '/^Cycles:/{print $2; exit}')" + + echo "==> [${P}/${N}tx] proving inner continuation (epoch=2^${EPOCH_LOG2}) ..." >&2 + rm -f /tmp/recursion_input.bin + DLOG="$WORK/dump_${N}tx_${P}.log" + if ! ( RECURSION_DUMP_PRESET="$P" RECURSION_DUMP_EPOCH_LOG2="$EPOCH_LOG2" \ + RECURSION_DUMP_INNER_ELF="$PWD/$ETHREX" RECURSION_DUMP_INNER_INPUT="$PWD/$FIX" \ + cargo test --release -p lambda-vm-prover ${DUMP_FEATURES:+--features "$DUMP_FEATURES"} --lib test_dump_recursion_input -- --ignored --nocapture ) \ + >"$DLOG" 2>&1 || [ ! -f /tmp/recursion_input.bin ]; then + echo "txs=$N preset=$P inner_cycles=$ic DUMP_FAILED" >> "$RESULTS" + echo "ERROR: [${P}/${N}tx] dump failed; tail of $DLOG:" >&2 + tail -20 "$DLOG" >&2 + continue + fi + epochs="$(grep -o 'continuation epochs: [0-9]*' "$DLOG" | awk '{print $3}')" + BLOB="$WORK/blob_${N}tx_${P}.bin" + mv /tmp/recursion_input.bin "$BLOB" + sz="$(wc -c < "$BLOB" | tr -d ' ')" + + echo "==> [${P}/${N}tx] executing recursion-cont-${P}.elf (${epochs} epochs, ${sz} bytes) ..." >&2 + t0=$(date +%s) + if out="$("$CLI" execute "$ART/recursion-cont-${P}.elf" --private-input "$BLOB" --cycles 2>"$WORK/exec_${N}tx_${P}.err")"; then + t1=$(date +%s) + cyc="$(printf '%s\n' "$out" | awk -F': ' '/^Cycles:/{print $2; exit}')" + kec="$(printf '%s\n' "$out" | awk -F': ' '/^Keccak calls:/{print $2; exit}')" + line="txs=$N preset=$P inner_cycles=$ic epochs=$epochs blob=$sz cycles=$cyc keccak=$kec exec_wall_s=$((t1 - t0))" + echo "$line" >> "$RESULTS" + echo " $line" >&2 + else + echo "txs=$N preset=$P inner_cycles=$ic epochs=$epochs blob=$sz EXEC_FAILED" >> "$RESULTS" + echo "ERROR: [${P}/${N}tx] guest execute failed; tail of stderr:" >&2 + tail -10 "$WORK/exec_${N}tx_${P}.err" >&2 + fi + rm -f "$BLOB" + done +done + +echo "==> done. Results:" +cat "$RESULTS" From 9c87b8c6448a4224ad77d5d35a1cbbaa8471290a Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Fri, 17 Jul 2026 16:05:13 -0300 Subject: [PATCH 02/13] fix(recursion-bench): pin guest --bin, prune fixtures, harden scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: - build_guest_elf: pass --bin so a continuation build's superset features (`continuation min`) don't also rebuild the plain preset's bin into the same shared target-dir path a concurrent `make -j` job is writing to. - drop redundant ethrex_bench_{1,8,16}.bin fixtures (only _4 is read by tests); bench_recursion_scaling.sh regenerates them on demand. - bench_recursion_scaling.sh: don't let one failed cycle-count/grep abort the whole sweep under `set -e`; clean up its temp dir. - recursion.rs docs: list blowup4 among the fixed-preset ELFs. - recursion profile tests: check the guest's actual committed attestation against a trusted host recompute, not just that it ran without crashing — covers both the monolithic and continuation (real ethrex block) profile paths. --- Makefile | 10 ++- executor/.gitignore | 10 +-- executor/tests/ethrex_bench_1.bin | Bin 13341 -> 0 bytes executor/tests/ethrex_bench_16.bin | Bin 29082 -> 0 bytes executor/tests/ethrex_bench_8.bin | Bin 21410 -> 0 bytes prover/src/recursion.rs | 7 +- prover/src/tests/recursion_smoke_test.rs | 84 ++++++++++++++++++++--- scripts/bench_recursion_scaling.sh | 5 +- 8 files changed, 96 insertions(+), 20 deletions(-) delete mode 100644 executor/tests/ethrex_bench_1.bin delete mode 100644 executor/tests/ethrex_bench_16.bin delete mode 100644 executor/tests/ethrex_bench_8.bin diff --git a/Makefile b/Makefile index 911edeb17..783817b2b 100644 --- a/Makefile +++ b/Makefile @@ -60,8 +60,13 @@ RECURSION_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/, $(addsuffix .elf # exactly one of its preset Cargo features at build time (fixes the inner # ProofOptions — see main.rs). Each preset builds its own distinctly named # [[bin]] (recursion--bench) to its own artifact, via the -# define/foreach/eval below rather than the generic %.elf pattern rule. The -# distinct bin names also make the per-preset `cp`s race-free under `make -j`. +# define/foreach/eval below rather than the generic %.elf pattern rule. +# `required-features` on each [[bin]] is a subset match, not an exact-set +# match, so e.g. `--features "continuation min"` (the cont-min build) also +# satisfies plain `recursion-min-bench`'s `required-features = ["min"]` and +# cargo builds it too — racing with a concurrent `make -j` job building +# `recursion-min.elf` (`--features min`) to that same shared-target-dir path. +# `--bin $(2)` in build_guest_elf pins each invocation to its one target bin. RECURSION_VERIFIER_PRESETS := min blowup2 blowup4 blowup8 # Continuation-verifying variants (the guest's `continuation` feature): verify a # multi-epoch ContinuationProof bundle instead of a monolithic VmProof. Kept to @@ -197,6 +202,7 @@ cd $(1) && \ -Z build-std=core,alloc,std,compiler_builtins,panic_abort \ -Z build-std-features=compiler-builtins-mem \ -Z json-target-spec \ + --bin $(2) \ $(3) cp $(SHARED_TARGET_DIR)/riscv64im-lambda-vm-elf/release/$(2) $@ endef diff --git a/executor/.gitignore b/executor/.gitignore index 3ed575591..ee277c65b 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -2,9 +2,9 @@ /program_artifacts/rust /tests/ethrex_hoodi.bin /tests/ethrex_bench_*.bin -# The small scaling-ladder fixtures ARE committed (scripts/bench_recursion_scaling.sh -# reads them; ~13-30 KB each). Bigger generated fixtures (e.g. _20) stay ignored. -!/tests/ethrex_bench_1.bin +# _4 is committed: prover/src/tests/recursion_smoke_test.rs reads it directly +# (~17 KB), and scripts/bench_recursion_scaling.sh also reads it as part of its +# scaling ladder. The other ladder sizes (_1/_8/_16) and bigger ones (e.g. _20) +# stay ignored — bench_recursion_scaling.sh generates any missing fixture on +# demand via tooling/ethrex-fixtures. !/tests/ethrex_bench_4.bin -!/tests/ethrex_bench_8.bin -!/tests/ethrex_bench_16.bin diff --git a/executor/tests/ethrex_bench_1.bin b/executor/tests/ethrex_bench_1.bin deleted file mode 100644 index 6a9c94380a13bb2d863e56b2e1cb3e75af75cb01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13341 zcmeHO3tUWF+h1$X{aUG%O65>VD7w0xN|aQ(m@bT(-A$1s5<+IW=zlziT_Lx~e0wCv=kU(FZ~c7E?|b*}H+w&8?Pvd=XRZIUp1ta9WOqE#q z&7+RqS6+CW?P_kV;HQ)OzQev~I}P_<{#osWC7coSW*=VoX4Cs1203F?+@x#gs({N}z{!L7Qj(us+Ko!~H9>ZHG#yZu_7l zlls2vLP|j42#s9RyIyID)>%m@iYpzCJAT3Z>YDG1ZXW~!q4wy{?SsQV3%bj&QT6Ti zQ3kbfMpX@D&=JlYP7j--&y__jojrhxfJfx>STgA3PxZnI$l^Gls+^Z>p`x&2Dsd{$ zk)zN5bkY(0$bh-f01ln%%+W`o8IC7~iS8J_8Sb;hCw`60(Z@~-6^TEFtaL`DI4thE zZ=3UNS;if9IioNPK_Q>R=7dSfqQ1^N$PY&%!Vx}xGYa8mK5FB{<|p#;33hgN2B?t? z8trT#yiQNJ``lzb$zCKDb_EI@Q5=hl;t&)Mb$O0h6-WQm0aexKJL;mSB6gJ#2bD2I ztvQ*(D{zj0ja`ExEN&Q%i%LqN&CYhHB%8zL@h~65ox^yTk`RxFS|dCb2ElVcWv~*2 zEKE{`d=!D-Y4Eo(FgVPCk4j?NCQ5uBYNW(NNpLm~)xiX!P#B2`1figc33#C(j|n)T zAc+ZxP#|LhgtKQ09dB`BZ-(3i%*P~W%mqhxn2B|k9S0LRSgY7#ks#|1FFV_D*eJw$ zAS?_@xx-dxJ3Ed$eBi`}D8k3FIT#$)t=33yc>O4pYQ^V0_21+j{mgr+koVNDc^4vm z%{%^ky!Y~N@)!T;C;o%J%ReH8-pBYy`uNxU#Q!H;9vbvr{)OE1MFI}8_zpsL9)4kG z(r0$e|08ym^|1qc*@^CDCr-!?{EMBS&+Ht1FA%t6ktczrn%UTKE_Qs2<(!!x1%hS}3 zk1|CiXORkp!sPk?dxP8va_3bpH)6hzffP4WRA)r@Dh$$#n3P%pM0PM&G-9Tx+hIC( zPxC+kAX1rB0vXZ}1Q19RDng@B$dH0iC=3P;p_AwcnTp5MAel+00UCoz!BHguAW<1) z3Ka(>-%f*Ek^W|`JYVU6_&SU%s(hcKr8G-wQD|9*R4Y3!A#l+Lk4=pz5`x`!jImBQ zj2(r2@h5gP@9mTy?4igY`$-z$ygUBsV9w)Cr}yzbLmqb8Rs}h&C`Xd(oSR4%B_3Wff&c+gs(zz)b z`zaA?j_>}{f%Ggg{=xa~367-^a~~k-fP_$IIgRvZ}pGqtr(ew?E!7 z-Ef&WR=hv>PiK$0TuP)K8GCKntxI=CZ+C1~3qMos+FC!s#lsH*RNwt>yPBbFx!HbP zcF4&#zs{F+JFcuIm3CdG86GT~4+ddL?gy;)Rzr}Idd32^KagKZS&U3~+A%q9Oq8{j z88_p_95vFTN<*e-Pw(_iPV4)Ge`)gTlSBfON~IulB8`rxLmZXBq~i$$gd_pz*f5}y zDL8~cCX)~{iAZ76NmM3Xf=OghByb25kBB)UUWHsXQQNi@^$#8r+UfAJ)-n24^XBxH z&bG=lkJ_$7qmk9v2>2cI>K`~FewC5=bNm2`wV?{*ADkZq#)L;ca5^1PR3u|ta7udW z>Ct`x*8VFwqGMHz5itO5>BoqGKud#V^^@jW59zX6 z5X=h`PENesm~QlSrMW#F)p_>lsO>ZjSLCTprFTHN+_F7? zw`v|VrKkC2&zoDxEp1`9fGm$P*%cM{+-5;w8`GBa{Gc3TPtcGT61P6eYCf^Zxw*%- zi1+$bOZKTv()|c6I*Ep5P%^|OAxs*D%%IRoWCjyYg%S*s1cO4r5hUo?p2!S zR4Nq@NkkG3B2>w@v$^pl$yZiiB*`AJ%8(Q$anX~JvKn)^{>|{o388EAZkOXq?t!DT zE5bA;t?s^oca6+ig7{;pcM1me^;(*v5c|j5rO)%0A8LrfgJ`w~LIaqu$}_JTwq~U9 zK7}2cj*YSY&ekMMxir){4gHPiO(-`=c~ZRWVg7Peb$H zGb^fJMaj>WX1@lGbMlff|2&+^XP4?mXrgw`%j?qIc=+PTYH&7m#m28Of51u2AABkhNBKIfH zF}$q#9Cvl$Si&4o7LhXiuxX1KrJ+s0RTBf_R|y;-R~mk<#<0WTd8{HQ%By4bsu{UX&CGZc>WbTpjaxlt zg2Qd2d@hzFD!TP1w95n07SR~}7!eR0FW$2%Hdl(wDMRt&n6)-#!Q%w>AG}Jwf2{Aol*c9uh%^S=jPPW|dyW+NchvoQTZQ@|;L!MSsukWiM z2+Y#5Srcr!cg%}+TykthxXh`C$(@M17#P1wq7>~1Fxc$NZ1(O-TecCkIz9Ntl)|YC z+}hYiw&lijuUHP2|7zAQrSu_W_$#TFI2JZ`ceH_ogd zp+%w*X*dFzLdP)y6W}GNbU>lei8wq12M_{Df=s92C2#~h1waWRLIw<~1VX}*0F?nC zLGrB)H-_L>?l#5~dwv>#r>CW#7q0exzU^mXN85jyiNpvva3iMrbxxj3ZP@GQMur8` z{G|)k1}R^v_Tl%u){Tz4I9m)1(ZTXlG{jk5Ft~X4B{@0oS?Yq6b=~BwlHGacn!65O zxMF+6lQpfVlK_F&0a|$M@R_sM=?ZhPu`^^c6rQvwv9x@TMM)TQnmDj~WF5Ev-v6x$a0g~zTiKAdc$ ze|y@&g|lQ9u1Y^x6ds*4M8h9~cNTl`i7z!SRCd&NB~4@;@-#`-pQ^sE{n4hn-!AG* zs}aMB=l}*ryl&aJXRpf5S~b-D%{A#WhS)>}8)=^S0Ky-}-cD5?7%N_O7g}#MPH)qN@}}mHCuDUKZJ=5GTfig&1q1ps?ekMhUvP+jPv>y}!qlJfT@+gSyzZ zxii~>ZIq;a-$0Ny|G~3As=J~FTV>m1t$lm!(b*W|nOY9dqeIWyugu(`+Kj~hT@hYtmK4lgQlx1T;!=f#d7XUIvhaXQ&5OoYh3d4E ziSZ4l+XuoyF))Dspizy-3?kp#8^5t?+PsxDcYn#qxrd4ktW!e0t0We#5eFmR??bt^ zrs}r*jIiVpODEU}U}FL_}QQ2`0Z4Tp_~!z;6F$MXnq_~pA&S{ zWYf_Fb6gD>Gj!_CH)gCN`4m*0lc5J%-4dfMU^&sCTsmgxE?d0>HJVdq#u1C{Eu`f; z#y*WSI-XJE-3CE@Lemp1&);O`RxU~^qC`n5&5KahYm=XDP;9p@Y3n>2F&@l;a>MD) zg0@xLs#FZ_nv~4GKEqhPdvETBmurU2XM2oGDHp?2k%^@rp#{PlMSkg0t0Wh=M^8IO zgSzidR+3gJZxnIxbI*E;#wO=F+2Hsi5bV8XA6)U|&nUMN?I*h!PjLLYMOhY*tCw_<%%ke*%T_(CRd zmYTFZzDj**Sn0zf4oXwA1Ykv%NGl{pXz}KeIfpjtFrvzjLjjo(6_hn`Rz}$1v26!b zc4D7yH+JFIoZ$NDj5hu-F*-2)#JS_$npOTEXdc(3BdAm4hd?m$m{sCq@2zdp>&N9k zFLNrBna<5GIu(1hWT!4ZS*6Md%zG!&kMb9fG_PKNJkO?Pnp1%8PCb2BBh#KLw8lJS zD>-HUh|%KT5YJud{owpjoj`!c(x=yr-f~}T_Wfg|ul!Yp{ZjRx315)@e)*SEUxdHw z{JrLjazBMH$bC5QY3KM)G7;Mg|3Blk!1_~}g6)NM-^}=+>}%eC54^9r{{t;ddaeKf diff --git a/executor/tests/ethrex_bench_16.bin b/executor/tests/ethrex_bench_16.bin deleted file mode 100644 index d466f9aba9ffa1bda3210b8fc262e639d9b5e51f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29082 zcmeHQ3tUXw_uuEt%$b=p=gg#sP{=Diq@yPPmNKTeJd=f=r3J4`7;rQ7Uc`phhf^Pr0~aXGTQ>hlV&ZreE%A9K z-)*IHA^I&U?oXiNQlTpT(2gph$n6$j$uGi^K;*&n~K z@x#Q+NQv7)m5?@v*n0f|1joqVjwb8Qd zYBD<{%yp#ug#D9y_xvE==h)D`6TXX~b73b`#803is8Gptpy~AqRc6r z_vX}k-&52!K;)AedRJZNJ|6ZyresFVD8yQ;rD$oZ@q2g2$rgdGvR3ET1=$=>KPIXh zK3KOifbW)cLdE$BR2(W)GvBrIYDz~6Hmz8SZk~RifUyLwp@io@$iNyYBI>2_td zyd*y(U17W5wWwFLBiDPo%y-*ftfv;~n6WLQDSb)I+w~iiN5-^k{C9zTRoDp?{u8Kh zDpV_4+EFd(NRg}eVrB$8YcL|3N9id4ZfMw0*VwcNStL%DJRa6pYX6ZTyGsL3yStB0 zH<;&YgV_%c;l*v4oi_AEA-+fi-lND=@3t9%&& zVvh9MLz%bsNa?uusk=r_sLh}BhqXm*hSoIk)gP*wK~e*L_K$UGysO*y$qb8;<6(C9 z{mN6{@95d<$3VWi(+L&pCs47dP@QXTN0r-=B5HaAH1C_r-JP%|-aqty&)Q1|m-UX+ zZCR%%)2eeS{8-hsjfr7W$L+}er@c~JtdeT>#g-nrLASgqa<<>YU~^TGuloMui%zJR zKY@x#h3dtdc2rL~QiOY}VS#lKZh;;>z0+NGjV7ibNUO0kjMaUy*vdii)`OZX-7yz6 z3m+szrzB|k*KYA$Ho9rV$)rP;FY1%ljM}LDV<2DM>4XaV2~-#rD(Mb1;vFeCx67;N z16xsApd*K$ztmSvAssy$q7yUupGH>o*pw!sdyJhW$tLuQ{8C0w8VV<2DM>4b{@ z6R7A^sFpUhqq6Ntkxh^GEfMov=C*jh#|Sz0h0V2(c`KLVN>+np%&+HdKO$bB?XJVw zbS;LDWv&C9l`RH6%{UCbh0@OU%S7WZR;>A|??1ligo^ePsAyEEVqdkR+SHMP)RrD{ zp-D~twueYx+U;5QMB@}sWuOk0ySbGlpK(s||MR1F$OFMW}B+O8_{e$F=Hfy!5X|M5j9 zRLD=DLa0#HH@2gC*pY&#vVW8}%?rjzqC9eFPMx-tnumvE#4W#synW-x9GLlJ2x9zb z_s)rUf@xK9ib>v$fll|q`Zt)Oq>^0DM%{HbKL+yEoldC0PoM%+sCspv5$Q+~4fcTY zy@~3tu!liUn%4&&9^|K?;@#_ZIxTQ)QN*?8q>;$=hh-~l1vXm-C7c>J`NLh^2lr&& z4>yr5BS$YEHGbV!egE+VGidBlo^F183lm?EIc&*{Yh@ZCk!h{jyQeTu?F)Ikpl`9> zqdNujSEu-8*c>Uo(x*&xs#HYa3bAL~r@WtZW%IP})hr#+_r)Pi!!v~wQg*kF?`wUp zP;Ps?c&?w{_QuyORcj6`(_egXGckraEzUgjRj{b@wfsd|<)^YkLt4F`th{ro-sI(- zy2L^J45PV=SbJu;9d@3?S~$AUzOVJpSE))vbI9+t`M$eUnH1C8dhz>}vIy`fyY38%Oi$_oQN$q~;Jamglz>=uqj@1_^M$aZ_WWfYCyZK0h(ZnDPL89XwtlTz^nzV zUz6663&XywnXd|Owh>}KuCmBf(1UO6x$avy-(HJbATLb?h&L)g!uaNV?;a8YTT?G2 z%m?-LLBr_b0t5*U6X=?xq30y?7XH3t^)RX-*I8P%{k$^wBD{MH!0KwMp*%TH|k25k;7hu;yP#-83ZPk~4e z!Cg}WfrvTZ+{+90(Z|%=3lg=}=OxfZy^LWMUWNj3c!eOwFeihA2~gxafWMW2p5BIG z0ue|(Njl6+Fh<%-z(kC^1S4SvYs<*N45KY012gotOfQ(BwPi$L25rl*U@7G8c*o5*Ek>Qv<%a0D&Xm?Tz%XK&ng)4EPqv zBNKB(fQHe``LH@Tth#8sc>PF}%7o{8>`(Gt(82fEHonJx$@jLJzT`Xodwg%_|D-?p zFZjs+72oAQ+CxwS`Hz0$zx+r3f2`L_u;RP?Z{y7;NuVLdVTNtodHk6><2$&c{g=4o z{E0hAJ9h%wxf9aH9pq2$tmxp*5xDiYfRe|9h7BW_n!)r9d_v8HrnYS`ZASTt-wNip z*NIefYQ(qSXuD2yt{6~VVf?gSuh8{&%KWU!;fx20Ipa0a(FU;z(T42PiBu0~N`frm zAGP1>;eVkF@$_XqyX@)dYi*;}!!wZzQb0R_sEY6S*g`-#z@F4KCQ?m@+u)qG5Cwq5 zaSS6_fX-%;TrP)>@&JQi(HVe_pZ-op`2H8}WZVXu_to9_hCVXt@u-5u-xyq2t%Qy-uI+w{t1F98N#K z!NJ%K)3z~5gsFb};QZi|a-9!^pa6M+q1k;85Wq#P@Z*O2Y7iX{#4vGQ>lZ~SsHk*a8Q8pWD1xhdpl!eow1|cji z!2ukoCp4S~023!!Y@CLOywO5Dsa3-DY>^>qYbcjp)Doj8J*&r(wPzZ7RGEitTDIif z!i~2D=xV3}8n9~zJ5*v1Otru5>Q~p@F>KvZ2RLg%f7+8@`^=;z)Ta02XZnT4ATKDI zBrfxcx7bF_JVD(GnBEl266EZoxbfo39=guX&r4?j{VOwkCr>Z+s_Y?tE?;NI@D_zLP@I%9|INvC~ zH*D*iyF+|VU7BBYZS35I_6QKDTHJp)L|vjla~~~vbxyVYv&QoHi))zqFE4P^_n)x_ zqLgSsOzf#?g4wBui`b6nRFn7#Axi6Y6*WB*Ui?0U`J_mlOWo71s06_S2!~DZ*fb^$ z@K`j2!@y8BhlU^s#>N2~BLJ6!bD^eSY%T|3GkGi&5Cj(oJUZI#)5Wq5-RRco0wkIb z-=ytDFGeJLdaUAQKIj$d~lnsNz>z_vHlw2K_P8{LS@#^_O`j z^^ZduMyy=DVk8(}oMovNZKe=&CjbF-#j(Y72McHZl74G_^bewnA>(ocZYu|?yclV9 z(T+$)z$?B2_ra8#DF?4ecu#nGHFvS+#>OG59CAMhGy?2IP@x{N7cM!nX=cbP?!EbL zBQFdcpJVS~RFWLGLoNA;Tj8*f%hb~H%TR!G566^=@1D2?Z<$*_ON)NJ>RPO?MDI%( zyLJvYf4*~SZg=#w3%WtWQ3Bzz5iX7pET{(zlFniQ46dqh9KjHH14BtR6F~?%fzf#^ z9uuPj4$7l(5gg7I|BaQ@e`pm1j#EC2{~5HPbXcZ(%2@qe>m*CVT`5;0S8TUh-0S2h z-T?C&hmxSnBfCTJsq5&1Y9Qcv%{*U4HH%91@KABD=Tftricf7l9Pj;Vb#}C}v1)x9 zb@K?2J}YfeDd1ls9U+25~{QDuZ)(qSq)Yw|8_xv(p zr93QUeZ+DExXc}{HOTCGp^b8`Hg0`$kJgT@tt%v4=2bNtq7ep%B)X@kDI14x-rv7! zmFj!mJkPancB=G_D|nXc_#)Rq?6mIOeQ+DTL_vGjguyMZPCijTxGhOJMfy;~!vRnG zjgnm@y^p)+!Z-wsUIUC2&SsRH-iF90Y_+q!)c{Cd->b@v?B{uICWDOfQwX1?=FZI8-)*8ltrU+XiT_a<#BP0 zK{5#@9p|ulG@QTymxnSDE{l#aQ3S_XOa@$?(=nXJVKC?-Z;U)cbE7VCd^m2iNOYgPM3>(Yl1l+hAzOC|Eby3{H zvX=*jp=)5b+jigo!mS3iV*i&YfK=JqKBFI<6$MH@eveE}`etW~>t~!0n{slPy_2qk zE1z0zh1hCz!-#c3vB9xVL0_GerK11=eTuW*G-Vhk>zs~lV5&$4_jHP(rOsj(FYh*ksO19g0%Bc$+WUq}>9Yk2TRB7LqfhmU ztegr&Qf1Ahz#YoP2B;BBGmQy#y8S$ux#EJ#fd`w$2Dvm5=V#PeX~vXD4qt?Txmgc3 z?$R1x+_zpya;Nq*)uRRk7jIx8X2QZpUc$*G!zgy90?9!`p~%-AcJ zS`{q<#~)r|FB5fQcB@N~5Uqe0SrJGZL!`A1s)K z0R0WsDbM#ylDk&)e!;u(K|;Q6T1v$({cNwM6P3v)Hi~sYv{-Bs&K4Y$APAC%VFcXI za#4&;hoLbXfYacXmB(W-NR+|hut;bS#aJ{R!sW2J0M0G{Td9B6M$K9-#JY_mczyGSf-+yL)?W<37ZO8$oQ$Qth-6n!Cn@C|pBjng>W<7LB^w8v(@wHzX?!%e%N==557b5uJIp#uMu= z45~AF=Av1>MOr=#G*PW3KqS>31pyI^GXL50b}P%q)(%rWy{So0uEIK!by#Fhgw(;t zRf6*fV6smyx||Tww9aPLhk%n}zcU?Gx4sXtG?bC67OZ3+g?q=fVEdcey)ozAMCL~q zK2ACnwO|jpcbq@|L!(i3-{Kp-f2(p6Ob%3O76QE*a5iI+Y=X%mxpb)8B$G$788ilu zfS+6v#s7U0ByH)?UDiRavHU} zOXUyR_8Tc}m1Us!cJb>Y8=9pKif^*qiHY4ZN&GSv&|gIOx*%GBPT(XBIsxF0gNAY0IE%rda~aT0 z08oJA9E`=G1BAdZxbVX0a4!N#0y>%*&=Q0(x_w#o{}pDjhObfeDr=9)@eIelhU|un zv9HnKS`KkmZI)-_8+6$*Y6S#Z|C*=&FCw(mV~PO&Q?DTl7c5S--Vl^LjJ6aq$hM>%c9Gzp5%G|g4lne6$D7;`(BQy)LKPN@lR>Qu^A=P(@gGiLnq;@Zjr%T7S03*s}*d{7oh!> zuTA1ypzdjRP;@~fpq-OPz-Uz_j3{D~IDyijrH6o?ah$~>a2RXEW3t&?7^laiLrW-& z2MgF71~ibu;8p~>!n-*F>95Qdna6jzt-jo&Y-08HE2~8B#l4@vIXZXh@r!|j?kZkF zW9JJo!XPqC=Jn0;ryEvJKBRsp(0+thPh)JtjD5DNZdo+U7(Jj`m^${NPcPwD_ZBi* zHRFCsg_;wK(QJwq*osG}R^f`6zLpcZb+y=(O{&N*11bF%04iCI$? zFL)@qv0$g#abx+i9ge`6$~f9B-ei!UQeLOHjb)*@_wMw{sm{KVwIbYGg%SM?(mkVE z(?T@6pd0_xP{v6fo;%{P)~$I>bQ{a9$y=(Al(AR)>WnFmxgc`!`pro&CWm$2B75os z=>==Df@VoYCfxQ~?9epOt@omNLrliE3@RcdB z8?jvS;&AWy%x=K~;@8^gt0}nXty=fw<*vncvXQF|J^7sBdIRTr^l=&UWS$VM0GGgi zV?2~}vku;Hjb##ZC0C1@J@oo_#{r4wz7gqkh zHgAUJ)-1i8>A!`>y{~_^(|JbO}8*tv`bMi5`l}Q<-q?%F*c2 zBUJTmd-+p0-z^lN-%jbX2?72OOq%SBdQ2|ZG}HBb%cNshC)!)5pNgxPxKfB#fYc@K z{VPj7jP@NJ^mP2{tliut*1DYBH&x}*PkwuSIcKymb%1~k6TJdkdWfNn(AdZU!@>

a8|vtSXrVkD`ZC!#k3}*8fZW1P|vhc!0)% z@nLj$9ED9{zrc*nH0f9|6Gp+&}8+0WqXY%EhbK7oT~8o8@(mnzd3 z>{K*r5|DV3wl^iSzG|+!#?xPBJiuGc(AcHT!|$9M~Ueq`|l1R!Y*))C;j2)fSHb3d;k5(0m@vq{s54*?5cW2Qqx3lhjj- zWS#BG`1Za$Y6u8W>ofAWezv_&;3|!SQF^MkwD#N061Q{Pwm;h^AhP#R2Lv=v+fCox zIP_dm!?l-@~>pm-XbtGYf^UMB9&hOoHuYei>`14{ZRoUzHTb@r?7 z6C#8`N{GcS$ZGg7G*=M*cCym&-M?+ft>a8f2IYa(W;3f@^rXdXUn3yS`ceHKmtL;# zsgta?)8oz2CwUt*W-1y!4_KQw+chy>rVFA)pg4&l2+D*Z&?JfAOcs+45BAbYCXEiD z#|fU+!gw&Qm`)?1>z@s91Yy!)cw1Xk908+=f03AI1SAz~y4KHWhlcUls?AFNvCHmV zkTt42>S&SkdRb{7q@@f2cG9;kuDtPT5L3uP&)j=*)hLYE1qM22HqXo+qT#Rz3TF7+ zldcE7ej7I~lK<4ZMBV?7Q6YWTi)WYwuAhH6`l>O+{(aNMpQY3kU|@F`_$70&Ml-t# zr|hw`X!hvPMzMwJ){m9mZt>&HP*h7i@q)S^LQnwh{gkEm11qWq zs*i3KOS$ztOzxuQ9AWC%zpzJduRK5R__c?Ej9MQ|)e)^7C-?AT#XA|LTiFUR*M+HL zbB(>V=xuR>134}zw|OgGPZKir6Sv)z|bxzH5O0!$wC7{Is-Iupiq!)E|AfYL}vK%mfZ_6tpa zgZrB7H5(ea#9;M0{ee4dRo;;z_mfn%&dvjtvy~fk9t$(hhv|pnkR$6vl{V&D&d$1U z=jg%6xo%YwS}Cp~^1%&{)eta@KhY>LsnmPgZ)TN-hl=B?a&s#N+_Uv`N6x;|S^l6- zm{$57TZ#0Kn*VfA^J`wv%F@y@bb!`<9o4=5uiSI0OcdbJgs6(rDA5^p)fbxsj2y{J zkLGA@(O-0C=Mm>VJ~mTS9hVEy3Z-NUp&`I$?V{W;gE9b4Vgc#(SDhAIRw_9hEP6F% zL=Iw(XPKvh)X_E9yzmf0Gwb?$_3F*e(mD`$4#xSDRvTB{PColp_;hwa}L zjF6X!KAhkkHF){Bl;y{qeYfa|GQye`8V}Ga@)D*WVAZr!y_YJ{m{=WBj|)1c(zZU=AISGKNHUK1g%Yojv_JM&=uATfbRIRgIh zNy#{tz>~Ip;J~hac=PS_-jy+9lGSIi(us=ID_3a?(MmT6-`^T_&qfiHVDn(e4%}~( zfPu2<92g49gYODrG2zyN2VXOU!Q;y)lgouSY4B}aTmlB^V;lte&zv@cs|?zu3e^AZ z)Q45x1;_2VZhCvoQZ9)4T|XmszAz*#@}FV!zo(UMdBavWs0Q_jb>*qs9UTOUni4M$ zUJS*=#{2Cb97OEjKhRDfabnqWkJr)Rezv3x0vhYLzjjN|oOrQzpB`>D#ZUdXn7#JY z?5aVMK~g;>he4w?IV<(5yY5P}GySc$d+AS_{Gl1U?iR#t*0M|8=eNXF838vmSkJ8+ zlq8?C%EyJeznuTR&PzMB$$7m`QCeoqBxzx69@J>T!OK10md2ej9d_ooV9mJ^CfbW? zLYqcZZ{OOw-gsrst5XHN7v(&ZTXb9WLdNKa6Ig}2)>Y0Z2Sc#_hV$cp#rDRW z8T0s}Mt|DGAz4A5fht*RkBNt_pZt4ezbjS?Bnl^3AYgLO;JB5Uk?+l;oD);x1-B*Zip-+Ue7jb|G5shvjjN&*U(9ap8!a}f9}Uz_k0Y9&6uRRB->}}3C75Ys~B(L Q&orX(^e4Fg2ZhG>g#Z8m diff --git a/executor/tests/ethrex_bench_8.bin b/executor/tests/ethrex_bench_8.bin deleted file mode 100644 index b5790e9492ee904614ed07b23ff6ca560846206d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21410 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 Rq8}Qd3;O=1TghMG{txSn0v`YX diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 2ac9fcfc0..4bd390655 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -20,9 +20,10 @@ //! //! [`program_id`] deliberately does not fold the `ProofOptions`: the security //! level is pinned by which verifier guest the outer proof is checked against -//! (`recursion-min.elf` vs `recursion-blowup2.elf`/`recursion-blowup8.elf`, -//! fixed at build time — see [`Preset`]). A consumer must pin that outer ELF -//! too, or a 1-query `min` attestation is indistinguishable from a 128-bit one. +//! (`recursion-min.elf` vs `recursion-blowup2.elf`/`recursion-blowup4.elf`/ +//! `recursion-blowup8.elf`, fixed at build time — see [`Preset`]). A consumer +//! must pin that outer ELF too, or a 1-query `min` attestation is +//! indistinguishable from a 128-bit one. use crypto::hash::platform_keccak::PlatformKeccak256 as Keccak256; use digest::Digest; diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index e95b1052a..d8c294d4d 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -155,9 +155,22 @@ fn drive_executor( (total_cycles, start.elapsed()) } +/// The identity + output a correct in-VM run must commit — the profile +/// tests' correctness oracle. Computed host-side (trustless recompute) before +/// the guest runs, then checked against the guest's actual committed +/// attestation once profiling finishes ([`run_profile_from`]). Mirrors the +/// production consumer check ([`recursion::check_attestation`]) for the +/// monolithic path, and its continuation analog +/// ([`crate::continuation::continuation_precomputed_commitments`] + +/// [`recursion::program_id_from_elf`]) for the continuation path. +struct ExpectedAttestation { + id: [u8; 32], + output: Vec, +} + /// Shared preamble: build the blob (an `empty` inner proof under the preset's /// options), load the `recursion-.elf` verifier, and stand up an -/// executor. Returns `(elf_bytes, program, executor)`. +/// executor. Returns `(elf_bytes, program, executor, expected_attestation)`. fn setup_guest_run( label: &str, preset: Preset, @@ -165,14 +178,21 @@ fn setup_guest_run( Vec, executor::elf::Elf, executor::vm::execution::Executor, + ExpectedAttestation, ) { let root = workspace_root(); let empty_elf_bytes = read_guest_elf(&root, "empty"); let guest_elf_bytes = read_guest_elf(&root, preset.artifact_stem()); - let (_inner_proof, blob) = + let (inner_proof, blob) = prove_inner_and_encode_blob(label, &empty_elf_bytes, &[], &preset.options()); + let expected = ExpectedAttestation { + id: recursion::expected_program_id(&empty_elf_bytes, &preset.options()) + .expect("expected_program_id errored"), + output: inner_proof.public_output, + }; + let program = executor::elf::Elf::load(&guest_elf_bytes).expect("ELF load failed"); assert_ne!( program.entry_point, @@ -182,7 +202,7 @@ fn setup_guest_run( ); let executor = executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); - (guest_elf_bytes, program, executor) + (guest_elf_bytes, program, executor, expected) } /// Read the ethrex guest ELF (built by `make executor/program_artifacts/rust/ethrex.elf`). @@ -206,7 +226,8 @@ fn read_ethrex_fixture(root: &std::path::Path, txs: u32) -> Vec { /// on `inner_input` via continuations (`epoch_log2`-cycle epochs) and loads /// `recursion-cont-.elf` instead of the monolithic `recursion-.elf` /// — the realistic in-VM cost of verifying a real (multi-epoch) inner proof -/// instead of the `empty`-program diagnostic floor. +/// instead of the `empty`-program diagnostic floor. Returns +/// `(elf_bytes, program, executor, expected_attestation)`. fn setup_continuation_guest_run( label: &str, preset: Preset, @@ -217,6 +238,7 @@ fn setup_continuation_guest_run( Vec, executor::elf::Elf, executor::vm::execution::Executor, + ExpectedAttestation, ) { let root = workspace_root(); let guest_elf_bytes = read_guest_elf(&root, &format!("recursion-cont-{}", preset.name())); @@ -230,6 +252,26 @@ fn setup_continuation_guest_run( crate::continuation::prove_continuation(inner_elf_bytes, inner_input, epoch_log2, &opts) .expect("inner continuation prove should succeed"); eprintln!("[{label}] continuation epochs: {}", bundle.num_epochs()); + + // Ground truth, computed on the bundle before `encode_continuation_guest_input` + // consumes it: a full trustless host verify (mirrors the monolithic pipeline's + // host `verify_with_options` check) for the expected output, plus the + // continuation analog of `check_attestation`'s recompute for the expected id. + let expected_output = + crate::continuation::verify_continuation(inner_elf_bytes, &bundle, &opts) + .expect("verify_continuation errored") + .expect("continuation bundle must verify on host before profiling the guest"); + let (expected_decode, expected_pages) = + crate::continuation::continuation_precomputed_commitments(inner_elf_bytes, &bundle, &opts) + .expect("continuation_precomputed_commitments errored"); + let expected_id = + recursion::program_id_from_elf(inner_elf_bytes, &expected_decode, &expected_pages) + .expect("program_id_from_elf errored"); + let expected = ExpectedAttestation { + id: expected_id, + output: expected_output, + }; + let blob = recursion::encode_continuation_guest_input(bundle, inner_elf_bytes, &opts) .expect("recursion::encode_continuation_guest_input failed"); eprintln!("[{label}] rkyv blob: {} bytes", blob.len()); @@ -243,7 +285,7 @@ fn setup_continuation_guest_run( ); let executor = executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); - (guest_elf_bytes, program, executor) + (guest_elf_bytes, program, executor, expected) } /// Demangled enclosing-function name for a PC via the ELF symbol table; @@ -392,7 +434,7 @@ fn print_step_breakdown(buckets: &[u64; 7], total_cycles: u64) { /// with `detailed`, also the top-25 functions table (needs a `pc_hist` /// HashMap, so gated). fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { - let (guest_elf_bytes, program, executor) = setup_guest_run("profile", preset); + let (guest_elf_bytes, program, executor, expected) = setup_guest_run("profile", preset); run_profile_from( preset, &guest_elf_bytes, @@ -400,6 +442,7 @@ fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { executor, progress_stride, detailed, + &expected, ); } @@ -415,7 +458,7 @@ fn run_profile_continuation( progress_stride: usize, detailed: bool, ) { - let (guest_elf_bytes, program, executor) = setup_continuation_guest_run( + let (guest_elf_bytes, program, executor, expected) = setup_continuation_guest_run( "profile-block", preset, inner_elf_bytes, @@ -429,6 +472,7 @@ fn run_profile_continuation( executor, progress_stride, detailed, + &expected, ); } @@ -442,6 +486,7 @@ fn run_profile_from( mut executor: executor::vm::execution::Executor, progress_stride: usize, detailed: bool, + expected: &ExpectedAttestation, ) { use std::collections::HashMap; @@ -499,6 +544,29 @@ fn run_profile_from( }, ); + // Correctness, not just crash-freedom: read the guest's actual committed + // attestation and check it against the trusted host recompute + // (`expected`, built by `setup_guest_run`/`setup_continuation_guest_run` + // before the guest ran) — the same identity+output binding a production + // consumer checks via `check_attestation`/its continuation analog. + let committed = executor + .finish() + .expect("read committed output after execution") + .memory_values; + let (id, output) = recursion::split_attestation(&committed) + .expect("attestation too short (guest committed fewer than 32 bytes)"); + assert_eq!( + id, expected.id, + "guest attestation program_id mismatch — in-VM verify accepted a different \ + (ELF, roots) identity than the trusted host recompute" + ); + assert_eq!( + output, expected.output.as_slice(), + "attested inner public output mismatch — the in-VM verify's committed output \ + diverges from the trusted host recompute" + ); + eprintln!("[profile] guest attestation matched the trusted host recompute (program_id + inner public output) ✓"); + eprintln!(); eprintln!("============================================================"); eprintln!( @@ -787,7 +855,7 @@ fn test_recursion_execute_1query() { #[test] #[ignore = "slow: runs the in-VM STARK verifier (minutes on CI)"] fn test_recursion_step_markers_observed_in_order() { - let (_bytes, program, mut executor) = setup_guest_run("step-markers", Preset::Min); + let (_bytes, program, mut executor, _expected) = setup_guest_run("step-markers", Preset::Min); let instructions = executor::vm::execution::InstructionCache::new(&program.data) .expect("instruction cache build failed"); diff --git a/scripts/bench_recursion_scaling.sh b/scripts/bench_recursion_scaling.sh index 3242c0b06..1472efa7d 100755 --- a/scripts/bench_recursion_scaling.sh +++ b/scripts/bench_recursion_scaling.sh @@ -43,6 +43,7 @@ PRESETS="${PRESETS:-blowup2 blowup4 min}" EPOCH_LOG2="${EPOCH_LOG2:-21}" RESULTS="${1:-/tmp/recursion_scaling.txt}" WORK="$(mktemp -d /tmp/recursion_scaling.XXXXXX)" +trap 'rm -rf "$WORK"' EXIT CLI=target/release/cli ART=executor/program_artifacts/recursion @@ -71,7 +72,7 @@ for P in $PRESETS; do # Inner block cost, once per block size (cheap; repeated per preset is fine — # the count is deterministic and the run takes well under a second). - ic="$("$CLI" execute "$ETHREX" --private-input "$FIX" --cycles | awk -F': ' '/^Cycles:/{print $2; exit}')" + ic="$("$CLI" execute "$ETHREX" --private-input "$FIX" --cycles | awk -F': ' '/^Cycles:/{print $2; exit}')" || true echo "==> [${P}/${N}tx] proving inner continuation (epoch=2^${EPOCH_LOG2}) ..." >&2 rm -f /tmp/recursion_input.bin @@ -85,7 +86,7 @@ for P in $PRESETS; do tail -20 "$DLOG" >&2 continue fi - epochs="$(grep -o 'continuation epochs: [0-9]*' "$DLOG" | awk '{print $3}')" + epochs="$(grep -o 'continuation epochs: [0-9]*' "$DLOG" | awk '{print $3}')" || true BLOB="$WORK/blob_${N}tx_${P}.bin" mv /tmp/recursion_input.bin "$BLOB" sz="$(wc -c < "$BLOB" | tr -d ' ')" From 3131de9c4ed5541237e3c0b84e25304cac1263a8 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Fri, 17 Jul 2026 19:38:45 -0300 Subject: [PATCH 03/13] fmt --- prover/src/tests/recursion_smoke_test.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index d8c294d4d..9593e316a 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -257,10 +257,9 @@ fn setup_continuation_guest_run( // consumes it: a full trustless host verify (mirrors the monolithic pipeline's // host `verify_with_options` check) for the expected output, plus the // continuation analog of `check_attestation`'s recompute for the expected id. - let expected_output = - crate::continuation::verify_continuation(inner_elf_bytes, &bundle, &opts) - .expect("verify_continuation errored") - .expect("continuation bundle must verify on host before profiling the guest"); + let expected_output = crate::continuation::verify_continuation(inner_elf_bytes, &bundle, &opts) + .expect("verify_continuation errored") + .expect("continuation bundle must verify on host before profiling the guest"); let (expected_decode, expected_pages) = crate::continuation::continuation_precomputed_commitments(inner_elf_bytes, &bundle, &opts) .expect("continuation_precomputed_commitments errored"); @@ -561,11 +560,14 @@ fn run_profile_from( (ELF, roots) identity than the trusted host recompute" ); assert_eq!( - output, expected.output.as_slice(), + output, + expected.output.as_slice(), "attested inner public output mismatch — the in-VM verify's committed output \ diverges from the trusted host recompute" ); - eprintln!("[profile] guest attestation matched the trusted host recompute (program_id + inner public output) ✓"); + eprintln!( + "[profile] guest attestation matched the trusted host recompute (program_id + inner public output) ✓" + ); eprintln!(); eprintln!("============================================================"); From 9ce7c7fd40b55335e9f2bd923954309c7e17456d Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Fri, 17 Jul 2026 19:49:53 -0300 Subject: [PATCH 04/13] address feedback + unreliable time in comments --- .github/workflows/bench-verify.yml | 47 ++++++++++++++++-------------- scripts/bench_recursion_cycles.sh | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index e79022082..c4ca8fa15 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -26,10 +26,9 @@ jobs: startsWith(github.event.comment.body, '/bench-verify') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) runs-on: [self-hosted, bench] - # Job cap. The verifier bench is ~5-6 min and the recursion measurement itself ~1 min, - # but on a cold runner the recursion BUILDS dominate: MEASURE_CLI (release cli) once, - # plus PER REF a guest build (~10-20 min) and a prover-test build for the blob dump. - # All cached in /tmp for later runs, and build-std / the host cargo target are shared + # Job cap. On a cold runner the recursion BUILDS dominate: MEASURE_CLI (release cli) + # once, plus PER REF a guest build and a prover-test build for the blob dump. All + # cached in /tmp for later runs, and build-std / the host cargo target are shared # across ref worktrees (see the recursion step's env) to keep the cold run under this # cap. The recursion step also has its own tighter timeout so a runaway build there # can't burn the whole job and lose the already-captured verifier result. @@ -46,7 +45,7 @@ jobs: await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: '⏳ **Benchmark started** on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.' + body: '⏳ **Benchmark started** on the bench server. The recursion-guest cycle comparison adds guest builds on top of the verifier bench, longer on a cold runner. The bench server is occupied until it finishes.' }); - name: Resolve PR head + pair count @@ -96,9 +95,8 @@ jobs: # in three regimes: `min` (blowup=2, 1 query — cheap diagnostic floor) plus the # realistic full-query base-layer points `blowup2` (219 queries) and `blowup4` # (110 queries). Each measurement is one exact `execute --cycles` reading per ref - # (no ABBA; the full-query executes are ~1-2 min each). - # Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli) once, plus - # per ref a guest build (~10-20 min) and a prover-test build for the blob dump; the + # (no ABBA). Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli) + # once, plus a guest build and a prover-test build for the blob dump per ref; the # GUEST_TARGET_DIR / HOST_TARGET_DIR below share build-std and the host cargo target # across the two ref worktrees so the second ref is much cheaper (and per-worktree # disk shrinks). continue-on-error + `!cancelled()` keep this fully isolated from the @@ -108,11 +106,9 @@ jobs: id: recursion if: ${{ !cancelled() }} continue-on-error: true - # Fail-fast well under the 90-min job cap so a runaway recursion build can't burn + # Fail-fast well under the job cap so a runaway recursion build can't burn # the whole job and lose the already-captured verifier result (continue-on-error - # absorbs the timeout; the job still posts the verifier verdict). Sized with - # headroom over a cold shared-build run (MEASURE_CLI + 2 guest builds + 2 blob-dump - # builds). + # absorbs the timeout; the job still posts the verifier verdict). timeout-minutes: 70 env: HEAD_SHA: ${{ steps.cfg.outputs.head_sha }} @@ -128,16 +124,23 @@ jobs: # Full-query regimes: the realistic base-layer proofs (the single-query `min` # regime above is a diagnostic floor, not the real verifier cost). Guest builds # and worktrees are already cached from the min run, so each adds only a blob - # dump (a full-query prove of the empty inner program, seconds) and one - # ~1-2 min execute per ref. Tolerated failure (else-note): refs predating the - # preset-aware dump test can only measure `min`; the min table above still posts. - for preset in blowup2 blowup4; do - if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then - { echo; sed -n '/=== Recursion-guest cycle/,$p' "/tmp/recursion_out_${preset}.txt"; } >> /tmp/recursion_result.txt - else - { echo; echo "_(${preset} full-query regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt - fi - done + # dump and one execute per ref. Both refs' dump tests must understand + # RECURSION_DUMP_PRESET to produce a comparable blob for a given preset — until + # `origin/main` merges the preset-aware dump test, it can only ever dump `min` + # options, so every blowup2/blowup4 attempt against it would fail the same way. + # Check that support once, up front, instead of attempting (and failing) each + # preset in turn. + if git grep -q RECURSION_DUMP_PRESET origin/main -- prover/src/tests/ 2>/dev/null; then + for preset in blowup2 blowup4; do + if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then + { echo; sed -n '/=== Recursion-guest cycle/,$p' "/tmp/recursion_out_${preset}.txt"; } >> /tmp/recursion_result.txt + else + { echo; echo "_(${preset} full-query regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt + fi + done + else + { echo; echo "_(blowup2/blowup4 full-query regimes need \`origin/main\` to have the preset-aware dump test (RECURSION_DUMP_PRESET) — not merged yet, so only \`min\` is compared for this PR.)_"; } >> /tmp/recursion_result.txt + fi - name: Post result if: always() diff --git a/scripts/bench_recursion_cycles.sh b/scripts/bench_recursion_cycles.sh index c6ae76058..2235c9158 100755 --- a/scripts/bench_recursion_cycles.sh +++ b/scripts/bench_recursion_cycles.sh @@ -206,7 +206,7 @@ measure_ref() { # 2a. Build the recursion guest ELF(s) (+ empty.elf inner program). GUEST_TARGET_DIR, # when set, shares the RV64 build dir across ref worktrees (reuses build-std). - echo "==> [$role] make compile-recursion-elfs @ $sha8 (this can take 10-20 min the first time) ..." >&2 + echo "==> [$role] make compile-recursion-elfs @ $sha8 (slow the first time) ..." >&2 local glog="$WORK/build_guest_${sha8}.log" local -a make_args=(compile-recursion-elfs) if [ -n "${GUEST_TARGET_DIR:-}" ]; then From ed9d34f1f05b822a27164b7dde7fbebe3356fb51 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Fri, 17 Jul 2026 19:52:47 -0300 Subject: [PATCH 05/13] feedback --- bench_vs/lambda/recursion/src/main.rs | 9 ++++----- prover/src/tests/recursion_smoke_test.rs | 8 ++++++++ scripts/bench_recursion_scaling.sh | 20 ++++++++++++++------ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/bench_vs/lambda/recursion/src/main.rs b/bench_vs/lambda/recursion/src/main.rs index 9e190ad14..116ba54fd 100644 --- a/bench_vs/lambda/recursion/src/main.rs +++ b/bench_vs/lambda/recursion/src/main.rs @@ -16,7 +16,7 @@ //! `ContinuationProof` bundle on the same wire format //! (`recursion::ContinuationGuestInput`, built by //! `recursion::encode_continuation_guest_input`), verified via -//! `recursion::verify_continuation_and_attest_blob` — same trust model; the +//! `recursion::verify_continuation_and_attest` — same trust model; the //! bundle is materialized with one rkyv deserialize pass (zero-copy epoch //! verify is follow-up work). //! @@ -96,10 +96,9 @@ pub fn main() -> ! { .expect("inner proof failed verification"); #[cfg(feature = "continuation")] - let attestation = - lambda_vm_prover::recursion::verify_continuation_and_attest_blob(blob, &options) - .expect("verify errored") - .expect("inner continuation proof failed verification"); + let attestation = lambda_vm_prover::recursion::verify_continuation_and_attest(blob, &options) + .expect("verify errored") + .expect("inner continuation proof failed verification"); lambda_vm_syscalls::syscalls::commit(&attestation); lambda_vm_syscalls::syscalls::sys_halt(); diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index 9593e316a..1fe7b9c91 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -1000,6 +1000,14 @@ fn test_dump_recursion_input() { let blob = match std::env::var("RECURSION_DUMP_EPOCH_LOG2") { Ok(s) => { + // No recursion-cont-blowup8.elf is built (RECURSION_CONT_PRESETS stops + // at blowup4), so this blob would have no guest to verify it. + assert_ne!( + preset, + Preset::Blowup8, + "RECURSION_DUMP_PRESET=blowup8 has no recursion-cont-blowup8.elf guest; \ + continuation mode only supports min|blowup2|blowup4" + ); let epoch_log2: u32 = s .parse() .unwrap_or_else(|e| panic!("bad RECURSION_DUMP_EPOCH_LOG2 '{s}': {e}")); diff --git a/scripts/bench_recursion_scaling.sh b/scripts/bench_recursion_scaling.sh index 1472efa7d..11a3d67f5 100755 --- a/scripts/bench_recursion_scaling.sh +++ b/scripts/bench_recursion_scaling.sh @@ -17,8 +17,8 @@ # Usage: scripts/bench_recursion_scaling.sh [RESULTS_FILE=/tmp/recursion_scaling.txt] # Env: # TXS="1 4 8 16" block sizes (transfers); fixtures are read from -# executor/tests/ethrex_bench_.bin (committed for -# 1/4/8/16) and generated via tooling/ethrex-fixtures +# executor/tests/ethrex_bench_.bin (only _4 is +# committed) and generated via tooling/ethrex-fixtures # when missing. # PRESETS="blowup2 blowup4 min" verifier presets, most important first. # EPOCH_LOG2=21 inner continuation epoch size (log2 cycles). @@ -70,9 +70,12 @@ for P in $PRESETS; do tooling/ethrex-fixtures/target/release/ethrex-fixtures "$N" "$FIX" distinct >&2 fi - # Inner block cost, once per block size (cheap; repeated per preset is fine — - # the count is deterministic and the run takes well under a second). - ic="$("$CLI" execute "$ETHREX" --private-input "$FIX" --cycles | awk -F': ' '/^Cycles:/{print $2; exit}')" || true + # Inner block cost, once per block size (cheap; deterministic). + if ! ic="$("$CLI" execute "$ETHREX" --private-input "$FIX" --cycles | awk -F': ' '/^Cycles:/{print $2; exit}')" || [ -z "$ic" ]; then + echo "txs=$N preset=$P inner_cycles=FAILED" >> "$RESULTS" + echo "ERROR: [${P}/${N}tx] inner cycle measurement failed for $FIX" >&2 + continue + fi echo "==> [${P}/${N}tx] proving inner continuation (epoch=2^${EPOCH_LOG2}) ..." >&2 rm -f /tmp/recursion_input.bin @@ -86,7 +89,12 @@ for P in $PRESETS; do tail -20 "$DLOG" >&2 continue fi - epochs="$(grep -o 'continuation epochs: [0-9]*' "$DLOG" | awk '{print $3}')" || true + epochs="$(grep -o 'continuation epochs: [0-9]*' "$DLOG" | awk '{print $3}')" + if [ -z "$epochs" ]; then + echo "txs=$N preset=$P inner_cycles=$ic EPOCHS_PARSE_FAILED" >> "$RESULTS" + echo "ERROR: [${P}/${N}tx] could not parse epoch count from $DLOG" >&2 + continue + fi BLOB="$WORK/blob_${N}tx_${P}.bin" mv /tmp/recursion_input.bin "$BLOB" sz="$(wc -c < "$BLOB" | tr -d ' ')" From d038f5c84bf3225e3d4ee459476cb847a255766d Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 14:59:41 -0300 Subject: [PATCH 06/13] perf(recursion-bench): pre-prove block continuation input as fixture test_recursion_profile_blowup4_block previously re-proved a real ethrex block's continuation bundle on every run (minutes of prover work) just to profile the verifier guest. Build the input once via a new recursion-profile-block-input Makefile target and read it as a fixture, with an .expected sidecar carrying the id+output ground truth so the test can verify the guest's attestation without re-deriving it. --- Makefile | 22 ++- prover/src/tests/recursion_smoke_test.rs | 192 ++++++++++------------- 2 files changed, 101 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index 783817b2b..d87e43ad2 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \ clean-recursion-elfs clean test test-asm \ test-rust test-ethrex test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \ -test-profile-recursion-block \ +test-profile-recursion-block recursion-profile-block-input \ test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \ test-prover-cuda test-prover-comprehensive-cuda \ bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \ @@ -298,8 +298,26 @@ test-profile-recursion-single: compile-recursion-elfs test-profile-recursion-multi: compile-recursion-elfs cargo test --package lambda-vm-prover --lib test_recursion_profile_multiquery -- --ignored --nocapture +# Pre-proved continuation input for test_recursion_profile_blowup4_block: +# proving a real ethrex block's continuation bundle is real prover work (not +# the verifier-guest cost that test profiles), so it's built ONCE here and +# include_bytes!'d by the test, rather than re-proven on every test run. +# Epoch=2^21 matches scripts/bench_recursion_scaling.sh's default. +RECURSION_PROFILE_BLOCK_INPUT := $(RECURSION_ARTIFACTS_DIR)/recursion-cont-blowup4-block4.bin + +recursion-profile-block-input: $(RECURSION_PROFILE_BLOCK_INPUT) + +$(RECURSION_PROFILE_BLOCK_INPUT): $(RUST_ARTIFACTS_DIR)/ethrex.elf executor/tests/ethrex_bench_4.bin | $(RECURSION_ARTIFACTS_DIR) + rm -f /tmp/recursion_input.bin /tmp/recursion_input.bin.expected + RECURSION_DUMP_PRESET=blowup4 RECURSION_DUMP_EPOCH_LOG2=21 \ + RECURSION_DUMP_INNER_ELF=$(CURDIR)/$(RUST_ARTIFACTS_DIR)/ethrex.elf \ + RECURSION_DUMP_INNER_INPUT=$(CURDIR)/executor/tests/ethrex_bench_4.bin \ + cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture + mv /tmp/recursion_input.bin $@ + mv /tmp/recursion_input.bin.expected $@.expected + # Real-block profile (ethrex, blowup=4/4 transfers), via the `continuation` guest. -test-profile-recursion-block: compile-recursion-elfs $(RUST_ARTIFACTS_DIR)/ethrex.elf +test-profile-recursion-block: compile-recursion-elfs $(RECURSION_PROFILE_BLOCK_INPUT) cargo test --package lambda-vm-prover --lib --release test_recursion_profile_blowup4_block -- --ignored --nocapture # Regenerate the committed ethrex block fixtures (see tooling/ethrex-fixtures). diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index 1fe7b9c91..e69b60206 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -205,82 +205,50 @@ fn setup_guest_run( (guest_elf_bytes, program, executor, expected) } -/// Read the ethrex guest ELF (built by `make executor/program_artifacts/rust/ethrex.elf`). -fn read_ethrex_elf(root: &std::path::Path) -> Vec { - let path = root.join("executor/program_artifacts/rust/ethrex.elf"); - std::fs::read(&path).unwrap_or_else(|e| { - panic!( - "failed to read {} — run `make executor/program_artifacts/rust/ethrex.elf`: {e}", - path.display() - ) - }) -} - -/// Read a committed ethrex block fixture (`executor/tests/ethrex_bench_.bin`). -fn read_ethrex_fixture(root: &std::path::Path, txs: u32) -> Vec { - let path = root.join(format!("executor/tests/ethrex_bench_{txs}.bin")); - std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())) -} - -/// [`setup_guest_run`] for the `continuation` guest feature: proves `inner_elf` -/// on `inner_input` via continuations (`epoch_log2`-cycle epochs) and loads -/// `recursion-cont-.elf` instead of the monolithic `recursion-.elf` -/// — the realistic in-VM cost of verifying a real (multi-epoch) inner proof -/// instead of the `empty`-program diagnostic floor. Returns -/// `(elf_bytes, program, executor, expected_attestation)`. -fn setup_continuation_guest_run( - label: &str, - preset: Preset, - inner_elf_bytes: &[u8], - inner_input: &[u8], - epoch_log2: u32, -) -> ( +/// [`setup_guest_run`]'s fixture-based counterpart for a real ethrex block: +/// reads a pre-proved continuation input instead of proving one in-process. +/// Proving a real block's continuation bundle is minutes of real prover +/// work — the `recursion-profile-block-input` Makefile target does it ONCE +/// into `executor/program_artifacts/recursion/recursion-cont-blowup4-block4.bin` +/// (+ `.expected` sidecar: 32-byte id || inner public output, written by +/// `test_dump_recursion_input`), so this test only ever measures the verifier +/// guest, never the inner prove. +fn setup_block4_blowup4_guest_run() -> ( Vec, executor::elf::Elf, executor::vm::execution::Executor, ExpectedAttestation, ) { let root = workspace_root(); - let guest_elf_bytes = read_guest_elf(&root, &format!("recursion-cont-{}", preset.name())); + let guest_elf_bytes = read_guest_elf(&root, "recursion-cont-blowup4"); - let opts = preset.options(); - eprintln!( - "[{label}] proving inner continuation (blowup={}, fri_queries={}, epoch=2^{epoch_log2}) ...", - opts.blowup_factor, opts.fri_number_of_queries - ); - let bundle = - crate::continuation::prove_continuation(inner_elf_bytes, inner_input, epoch_log2, &opts) - .expect("inner continuation prove should succeed"); - eprintln!("[{label}] continuation epochs: {}", bundle.num_epochs()); - - // Ground truth, computed on the bundle before `encode_continuation_guest_input` - // consumes it: a full trustless host verify (mirrors the monolithic pipeline's - // host `verify_with_options` check) for the expected output, plus the - // continuation analog of `check_attestation`'s recompute for the expected id. - let expected_output = crate::continuation::verify_continuation(inner_elf_bytes, &bundle, &opts) - .expect("verify_continuation errored") - .expect("continuation bundle must verify on host before profiling the guest"); - let (expected_decode, expected_pages) = - crate::continuation::continuation_precomputed_commitments(inner_elf_bytes, &bundle, &opts) - .expect("continuation_precomputed_commitments errored"); - let expected_id = - recursion::program_id_from_elf(inner_elf_bytes, &expected_decode, &expected_pages) - .expect("program_id_from_elf errored"); + let art = root.join("executor/program_artifacts/recursion"); + let blob_path = art.join("recursion-cont-blowup4-block4.bin"); + let blob = std::fs::read(&blob_path).unwrap_or_else(|e| { + panic!( + "failed to read {} — run `make recursion-profile-block-input`: {e}", + blob_path.display() + ) + }); + let expected_path = art.join("recursion-cont-blowup4-block4.bin.expected"); + let expected_bytes = std::fs::read(&expected_path).unwrap_or_else(|e| { + panic!( + "failed to read {} — run `make recursion-profile-block-input`: {e}", + expected_path.display() + ) + }); + let (id_bytes, output) = expected_bytes.split_at(32); let expected = ExpectedAttestation { - id: expected_id, - output: expected_output, + id: id_bytes + .try_into() + .expect("expected sidecar id is 32 bytes"), + output: output.to_vec(), }; - let blob = recursion::encode_continuation_guest_input(bundle, inner_elf_bytes, &opts) - .expect("recursion::encode_continuation_guest_input failed"); - eprintln!("[{label}] rkyv blob: {} bytes", blob.len()); - let program = executor::elf::Elf::load(&guest_elf_bytes).expect("ELF load failed"); assert_ne!( - program.entry_point, - 0, - "recursion-cont-{} ELF has entry_point=0 — build artifact is malformed", - preset.name() + program.entry_point, 0, + "recursion-cont-blowup4 ELF has entry_point=0 — build artifact is malformed", ); let executor = executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); @@ -445,37 +413,7 @@ fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { ); } -/// [`run_profile`] over a REAL inner program instead of `empty`: verifies -/// `inner_elf_bytes`/`inner_input` via the `continuation` guest -/// (`recursion-cont-.elf`) — the realistic in-VM verifier cost, not -/// the intrinsic-overhead floor. -fn run_profile_continuation( - preset: Preset, - inner_elf_bytes: &[u8], - inner_input: &[u8], - epoch_log2: u32, - progress_stride: usize, - detailed: bool, -) { - let (guest_elf_bytes, program, executor, expected) = setup_continuation_guest_run( - "profile-block", - preset, - inner_elf_bytes, - inner_input, - epoch_log2, - ); - run_profile_from( - preset, - &guest_elf_bytes, - &program, - executor, - progress_stride, - detailed, - &expected, - ); -} - -/// Shared profiling loop for [`run_profile`]/[`run_profile_continuation`]: runs +/// Shared profiling loop for [`run_profile`]/`test_recursion_profile_blowup4_block`: runs /// an already-set-up guest executor and prints the same cycle/step/function /// breakdown regardless of which inner program it verified. fn run_profile_from( @@ -545,7 +483,7 @@ fn run_profile_from( // Correctness, not just crash-freedom: read the guest's actual committed // attestation and check it against the trusted host recompute - // (`expected`, built by `setup_guest_run`/`setup_continuation_guest_run` + // (`expected`, built by `setup_guest_run`/`setup_block4_blowup4_guest_run` // before the guest ran) — the same identity+output binding a production // consumer checks via `check_attestation`/its continuation analog. let committed = executor @@ -998,7 +936,13 @@ fn test_dump_recursion_input() { Err(_) => Vec::new(), }; - let blob = match std::env::var("RECURSION_DUMP_EPOCH_LOG2") { + // Continuation dumps also get an `.expected` sidecar (32-byte id || inner + // public output) — the ground truth a caller needs to check a guest's + // committed attestation, computed here while the `ContinuationProof` + // bundle still exists (`encode_continuation_guest_input` consumes it). + // Lets a consumer (e.g. `test_recursion_profile_blowup4_block`) verify + // the pre-proved fixture without re-deriving it from the bundle. + let (blob, expected_sidecar) = match std::env::var("RECURSION_DUMP_EPOCH_LOG2") { Ok(s) => { // No recursion-cont-blowup8.elf is built (RECURSION_CONT_PRESETS stops // at blowup4), so this blob would have no guest to verify it. @@ -1024,8 +968,25 @@ fn test_dump_recursion_input() { ) .expect("inner continuation prove should succeed"); eprintln!("[dump-input] continuation epochs: {}", bundle.num_epochs()); - recursion::encode_continuation_guest_input(bundle, &inner_elf_bytes, &opts) - .expect("recursion::encode_continuation_guest_input failed") + + let expected_output = + crate::continuation::verify_continuation(&inner_elf_bytes, &bundle, &opts) + .expect("verify_continuation errored") + .expect("continuation bundle must verify on host before dumping"); + let (expected_decode, expected_pages) = + crate::continuation::continuation_precomputed_commitments( + &inner_elf_bytes, + &bundle, + &opts, + ) + .expect("continuation_precomputed_commitments errored"); + let expected_id = + recursion::program_id_from_elf(&inner_elf_bytes, &expected_decode, &expected_pages) + .expect("program_id_from_elf errored"); + + let blob = recursion::encode_continuation_guest_input(bundle, &inner_elf_bytes, &opts) + .expect("recursion::encode_continuation_guest_input failed"); + (blob, Some((expected_id, expected_output))) } Err(_) => { let (_inner_proof, blob) = prove_inner_and_encode_blob( @@ -1034,7 +995,7 @@ fn test_dump_recursion_input() { &inner_input, &preset.options(), ); - blob + (blob, None) } }; assert!( @@ -1049,6 +1010,18 @@ fn test_dump_recursion_input() { preset.name(), blob.len() ); + + if let Some((id, output)) = expected_sidecar { + let mut sidecar_data = Vec::with_capacity(32 + output.len()); + sidecar_data.extend_from_slice(&id); + sidecar_data.extend_from_slice(&output); + let sidecar_path = format!("{path}.expected"); + std::fs::write(&sidecar_path, &sidecar_data).expect("write expected sidecar"); + eprintln!( + "[dump-input] wrote {} bytes to {sidecar_path}", + sidecar_data.len() + ); + } } /// Cycle count only of the recursion guest verifying a 1-query inner proof. @@ -1081,10 +1054,6 @@ fn test_recursion_cycles_blowup4() { run_profile(Preset::Blowup4, 500, false); } -/// Continuation epoch size for the real-block profile below — matches -/// `scripts/bench_recursion_scaling.sh`'s default. -const BLOCK_EPOCH_LOG2: u32 = 21; - /// Full profile (top-25 + per-step) of the recursion `continuation` guest /// verifying a REAL ethrex block (4 transfers) — not the `empty`-program /// diagnostic floor `test_recursion_profile_1query`/`_multiquery` measure. @@ -1092,19 +1061,20 @@ const BLOCK_EPOCH_LOG2: u32 = 21; /// presets, so the full histogram stays tractable; `bench_recursion_cycles.sh`/ /// `bench_recursion_scaling.sh` already cover cycle counts across every preset /// and block size, so this only needs to exist for the detailed breakdown. +/// Requires `make recursion-profile-block-input` — the pre-proved fixture +/// [`setup_block4_blowup4_guest_run`] reads. #[test] #[ignore = "diagnostic: heavy; recursion guest histogram + steps over a real ethrex block (blowup=4)"] fn test_recursion_profile_blowup4_block() { - let root = workspace_root(); - let ethrex_elf_bytes = read_ethrex_elf(&root); - let fixture = read_ethrex_fixture(&root, 4); - run_profile_continuation( + let (guest_elf_bytes, program, executor, expected) = setup_block4_blowup4_guest_run(); + run_profile_from( Preset::Blowup4, - ðrex_elf_bytes, - &fixture, - BLOCK_EPOCH_LOG2, + &guest_elf_bytes, + &program, + executor, 500, true, + &expected, ); } From ea16454119c5ed85f49d7807785fcb32c4c4f182 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 15:22:48 -0300 Subject: [PATCH 07/13] wire real-block recursion profiling/bench into CI, cache the dumped proof profile-recursion.yml runs test-profile-recursion-block; bench-verify.yml adds a blowup4-block regime. The dump test's proof blob is now cached by ref SHA so repeat runs skip re-proving. Also trims verbose comments. --- .github/workflows/bench-verify.yml | 56 ++++---- .github/workflows/profile-recursion.yml | 8 +- Makefile | 42 +++--- bench_vs/lambda/recursion/src/main.rs | 8 +- executor/.gitignore | 8 +- prover/src/recursion.rs | 12 +- prover/src/tests/recursion_smoke_test.rs | 71 ++++------ scripts/bench_recursion_cycles.sh | 161 ++++++++++++++++------- scripts/bench_recursion_scaling.sh | 10 +- 9 files changed, 195 insertions(+), 181 deletions(-) diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index c4ca8fa15..9969e12f6 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -26,12 +26,9 @@ jobs: startsWith(github.event.comment.body, '/bench-verify') && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) runs-on: [self-hosted, bench] - # Job cap. On a cold runner the recursion BUILDS dominate: MEASURE_CLI (release cli) - # once, plus PER REF a guest build and a prover-test build for the blob dump. All - # cached in /tmp for later runs, and build-std / the host cargo target are shared - # across ref worktrees (see the recursion step's env) to keep the cold run under this - # cap. The recursion step also has its own tighter timeout so a runaway build there - # can't burn the whole job and lose the already-captured verifier result. + # Job cap. On a cold runner the recursion BUILDS dominate: MEASURE_CLI once, plus + # per ref a guest build and a prover-test build. Cached in /tmp; build-std / host + # cargo target shared across ref worktrees (see the recursion step's env). timeout-minutes: 90 steps: - name: Acknowledge (react + occupancy notice) @@ -91,24 +88,20 @@ jobs: scripts/bench_verify.sh "$HEAD_SHA" origin/main "$PAIRS" 2>&1 | tee /tmp/verify_out.txt sed -n '/=== Verify ABBA result/,$p' /tmp/verify_out.txt > /tmp/verify_result.txt - # Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main), - # in three regimes: `min` (blowup=2, 1 query — cheap diagnostic floor) plus the - # realistic full-query base-layer points `blowup2` (219 queries) and `blowup4` - # (110 queries). Each measurement is one exact `execute --cycles` reading per ref - # (no ABBA). Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli) - # once, plus a guest build and a prover-test build for the blob dump per ref; the - # GUEST_TARGET_DIR / HOST_TARGET_DIR below share build-std and the host cargo target - # across the two ref worktrees so the second ref is much cheaper (and per-worktree - # disk shrinks). continue-on-error + `!cancelled()` keep this fully isolated from the - # verifier bench above: a failure OR timeout here never fails the job nor clobbers the - # verifier verdict, and it still runs if the verifier step failed. + # Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main), in + # four regimes: `min`, `blowup2`, `blowup4` (empty diagnostic inner program) plus + # `blowup4-block` (same verifier over a REAL ethrex block, via the `continuation` + # guest). One exact `execute --cycles` reading per ref (no ABBA); blowup4-block's + # dumped blob is cached by ref SHA (bench_recursion_cycles.sh), so a repeat run + # skips re-proving. GUEST_TARGET_DIR / HOST_TARGET_DIR share build-std and the + # host cargo target across ref worktrees. continue-on-error + `!cancelled()` + # isolate this from the verifier bench above. - name: Run recursion guest cycle benchmark id: recursion if: ${{ !cancelled() }} continue-on-error: true - # Fail-fast well under the job cap so a runaway recursion build can't burn - # the whole job and lose the already-captured verifier result (continue-on-error - # absorbs the timeout; the job still posts the verifier verdict). + # Fail-fast under the job cap so a runaway build can't burn the whole job + # (continue-on-error absorbs the timeout; the verifier verdict still posts). timeout-minutes: 70 env: HEAD_SHA: ${{ steps.cfg.outputs.head_sha }} @@ -121,15 +114,9 @@ jobs: set -o pipefail scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main min 2>&1 | tee /tmp/recursion_out.txt sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out.txt > /tmp/recursion_result.txt - # Full-query regimes: the realistic base-layer proofs (the single-query `min` - # regime above is a diagnostic floor, not the real verifier cost). Guest builds - # and worktrees are already cached from the min run, so each adds only a blob - # dump and one execute per ref. Both refs' dump tests must understand - # RECURSION_DUMP_PRESET to produce a comparable blob for a given preset — until - # `origin/main` merges the preset-aware dump test, it can only ever dump `min` - # options, so every blowup2/blowup4 attempt against it would fail the same way. - # Check that support once, up front, instead of attempting (and failing) each - # preset in turn. + # Full-query regimes, reusing the min run's cached guest builds/worktrees. + # origin/main needs RECURSION_DUMP_PRESET support to dump non-min blobs; + # check once up front instead of failing each preset in turn. if git grep -q RECURSION_DUMP_PRESET origin/main -- prover/src/tests/ 2>/dev/null; then for preset in blowup2 blowup4; do if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then @@ -141,6 +128,17 @@ jobs: else { echo; echo "_(blowup2/blowup4 full-query regimes need \`origin/main\` to have the preset-aware dump test (RECURSION_DUMP_PRESET) — not merged yet, so only \`min\` is compared for this PR.)_"; } >> /tmp/recursion_result.txt fi + # Real-block regime: needs origin/main to support RECURSION_DUMP_EPOCH_LOG2 + # (continuation dumps); checked once up front like the loop above. + if git grep -q RECURSION_DUMP_EPOCH_LOG2 origin/main -- prover/src/tests/ 2>/dev/null; then + if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main blowup4-block 2>&1 | tee /tmp/recursion_out_blowup4-block.txt; then + { echo; sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out_blowup4-block.txt; } >> /tmp/recursion_result.txt + else + { echo; echo "_(blowup4-block real-ethrex-block regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt + fi + else + { echo; echo "_(blowup4-block real-ethrex-block regime needs \`origin/main\` to support RECURSION_DUMP_EPOCH_LOG2 — not merged yet.)_"; } >> /tmp/recursion_result.txt + fi - name: Post result if: always() diff --git a/.github/workflows/profile-recursion.yml b/.github/workflows/profile-recursion.yml index 5fc9817ba..6829dfff3 100644 --- a/.github/workflows/profile-recursion.yml +++ b/.github/workflows/profile-recursion.yml @@ -42,6 +42,9 @@ jobs: - name: multi-query test: multi title: "Multi query (blowup=8, 128-bit)" + - name: block + test: block + title: "Real ethrex block, 4 transfers (blowup=4, 110 queries)" steps: - name: React to comment if: github.event_name == 'issue_comment' && matrix.name == 'single-query' @@ -136,9 +139,10 @@ jobs: { echo "## Recursion guest profile" echo - # Single-query first, then multi-query, then any others. + # Single-query first, then multi-query, then the real-block profile. for frag in fragments/fragment-single-query.md \ - fragments/fragment-multi-query.md; do + fragments/fragment-multi-query.md \ + fragments/fragment-block.md; do [ -f "$frag" ] && { cat "$frag"; echo; } done echo "Commit: ${COMMIT_SHA:0:8} · Runner: self-hosted bench" diff --git a/Makefile b/Makefile index d87e43ad2..4abed4a90 100644 --- a/Makefile +++ b/Makefile @@ -61,16 +61,14 @@ RECURSION_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/, $(addsuffix .elf # ProofOptions — see main.rs). Each preset builds its own distinctly named # [[bin]] (recursion--bench) to its own artifact, via the # define/foreach/eval below rather than the generic %.elf pattern rule. -# `required-features` on each [[bin]] is a subset match, not an exact-set -# match, so e.g. `--features "continuation min"` (the cont-min build) also -# satisfies plain `recursion-min-bench`'s `required-features = ["min"]` and -# cargo builds it too — racing with a concurrent `make -j` job building -# `recursion-min.elf` (`--features min`) to that same shared-target-dir path. -# `--bin $(2)` in build_guest_elf pins each invocation to its one target bin. +# `required-features` is a subset match, so e.g. `--features "continuation min"` +# also satisfies plain `recursion-min-bench`'s `required-features = ["min"]`, +# racing a concurrent `make -j` build of `recursion-min.elf` for the same +# shared-target-dir path. `--bin $(2)` in build_guest_elf pins each invocation +# to its one target bin. RECURSION_VERIFIER_PRESETS := min blowup2 blowup4 blowup8 -# Continuation-verifying variants (the guest's `continuation` feature): verify a -# multi-epoch ContinuationProof bundle instead of a monolithic VmProof. Kept to -# the presets the recursion benchmarks actually measure. +# `continuation` feature: verify a multi-epoch ContinuationProof bundle instead +# of a monolithic VmProof. Only the presets the benchmarks actually measure. RECURSION_CONT_PRESETS := min blowup2 blowup4 RECURSION_VERIFIER_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/recursion-, $(addsuffix .elf, $(RECURSION_VERIFIER_PRESETS))) \ $(addprefix $(RECURSION_ARTIFACTS_DIR)/recursion-cont-, $(addsuffix .elf, $(RECURSION_CONT_PRESETS))) @@ -227,21 +225,12 @@ $(BENCH_ARTIFACTS_DIR)/%.elf: FORCE | prepare-sysroot $(BENCH_ARTIFACTS_DIR) $(RECURSION_ARTIFACTS_DIR)/%.elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR) $(call build_guest_elf,$(RECURSION_GUESTS_DIR)/$*,$*-bench) -# The recursion verifier's presets (RECURSION_VERIFIER_PRESETS): same crate dir, one -# differently named [[bin]] per preset (recursion--bench, gated on that -# preset's Cargo feature) -> a differently named artifact. Generated per preset -# from RECURSION_VERIFIER_PRESETS via define/foreach/eval rather than a pattern -# rule (the stem "recursion-min" wouldn't match the crate dir "recursion") and -# rather than copy-paste (the presets list is the single source of truth). +# One differently named [[bin]] per preset (recursion--bench, gated on +# that preset's Cargo feature) -> a differently named artifact. define/foreach/ +# eval rather than a pattern rule (stem "recursion-min" wouldn't match crate +# dir "recursion") or copy-paste (presets list is the single source of truth). # $(1) is the preset; the recipe uses $$ so `$$(call build_guest_elf,...)` -# survives the $(call ...) expansion and is expanded at recipe-run time (where -# $@ is defined). Because the two bins have distinct filenames the post-build -# `cp`s read different files, so the `make -j` cp race is gone structurally and -# no `.NOTPARALLEL` is needed: cargo's target-dir lock already serializes the -# compiles, and `.NOTPARALLEL` with prerequisites was wrong on every make -# version anyway (it serializes the whole build on GNU make <= 4.3 — macOS ships -# 3.81, ubuntu-latest 4.3 — and on >= 4.4 serializes only the listed targets' -# own prerequisites, never the two ELF targets against each other). +# expands at recipe-run time (where $@ is defined). define recursion_verifier_rule $(RECURSION_ARTIFACTS_DIR)/recursion-$(1).elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR) $$(call build_guest_elf,$$(RECURSION_GUESTS_DIR)/recursion,recursion-$(1)-bench,--features $(1)) @@ -298,10 +287,9 @@ test-profile-recursion-single: compile-recursion-elfs test-profile-recursion-multi: compile-recursion-elfs cargo test --package lambda-vm-prover --lib test_recursion_profile_multiquery -- --ignored --nocapture -# Pre-proved continuation input for test_recursion_profile_blowup4_block: -# proving a real ethrex block's continuation bundle is real prover work (not -# the verifier-guest cost that test profiles), so it's built ONCE here and -# include_bytes!'d by the test, rather than re-proven on every test run. +# Pre-proved continuation input for test_recursion_profile_blowup4_block: proving +# a real ethrex block is real prover work, not the verifier-guest cost the test +# profiles, so it's built ONCE here rather than re-proven on every test run. # Epoch=2^21 matches scripts/bench_recursion_scaling.sh's default. RECURSION_PROFILE_BLOCK_INPUT := $(RECURSION_ARTIFACTS_DIR)/recursion-cont-blowup4-block4.bin diff --git a/bench_vs/lambda/recursion/src/main.rs b/bench_vs/lambda/recursion/src/main.rs index 116ba54fd..1a846109b 100644 --- a/bench_vs/lambda/recursion/src/main.rs +++ b/bench_vs/lambda/recursion/src/main.rs @@ -13,12 +13,10 @@ //! `VmProof`. //! //! The `continuation` feature swaps the monolithic proof for a multi-epoch -//! `ContinuationProof` bundle on the same wire format -//! (`recursion::ContinuationGuestInput`, built by +//! `ContinuationProof` bundle (`recursion::ContinuationGuestInput`, built by //! `recursion::encode_continuation_guest_input`), verified via -//! `recursion::verify_continuation_and_attest` — same trust model; the -//! bundle is materialized with one rkyv deserialize pass (zero-copy epoch -//! verify is follow-up work). +//! `recursion::verify_continuation_and_attest` — same trust model, one rkyv +//! deserialize pass (zero-copy epoch verify is follow-up work). //! //! `ProofOptions` is fixed by exactly one preset Cargo feature //! (`min`/`blowup2`/`blowup4`/`blowup8` — a `Preset`), not private input — an diff --git a/executor/.gitignore b/executor/.gitignore index ee277c65b..17f7497d7 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -2,9 +2,7 @@ /program_artifacts/rust /tests/ethrex_hoodi.bin /tests/ethrex_bench_*.bin -# _4 is committed: prover/src/tests/recursion_smoke_test.rs reads it directly -# (~17 KB), and scripts/bench_recursion_scaling.sh also reads it as part of its -# scaling ladder. The other ladder sizes (_1/_8/_16) and bigger ones (e.g. _20) -# stay ignored — bench_recursion_scaling.sh generates any missing fixture on -# demand via tooling/ethrex-fixtures. +# _4 is committed (~17 KB): used by `make recursion-profile-block-input` and +# scripts/bench_recursion_scaling.sh. Other sizes stay ignored — scaling.sh +# generates any missing fixture on demand via tooling/ethrex-fixtures. !/tests/ethrex_bench_4.bin diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 4bd390655..efca722c9 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -53,16 +53,12 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { pub enum Preset { /// Blowup=2, 1 query ([`MIN_PROOF_OPTIONS`]) — insecure, diagnostics only. Min, - /// Blowup=2, full 128-bit query count (219 queries at 20 grinding bits) — - /// the realistic base-layer shape: production pipelines prove the base - /// proof at low blowup (2/4) and reserve high blowup for the final wrap. + /// Blowup=2, 219 queries — 128-bit, realistic base-layer shape (low + /// blowup, high query count; final wrap uses high blowup instead). Blowup2, - /// Blowup=4, 110 queries — the other realistic base-layer point (e.g. - /// Zisk's compressor layer): 2× the prover LDE of blowup=2 for half the - /// queries to verify. + /// Blowup=4, 110 queries — the other realistic base-layer point. Blowup4, - /// Blowup=8, multi-query (73 queries) — 128-bit security at final-wrap-style - /// parameters: more prover work per row, far fewer queries to verify. + /// Blowup=8, 73 queries — 128-bit, final-wrap-style parameters. Blowup8, } diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index e69b60206..1f4800011 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -156,13 +156,8 @@ fn drive_executor( } /// The identity + output a correct in-VM run must commit — the profile -/// tests' correctness oracle. Computed host-side (trustless recompute) before -/// the guest runs, then checked against the guest's actual committed -/// attestation once profiling finishes ([`run_profile_from`]). Mirrors the -/// production consumer check ([`recursion::check_attestation`]) for the -/// monolithic path, and its continuation analog -/// ([`crate::continuation::continuation_precomputed_commitments`] + -/// [`recursion::program_id_from_elf`]) for the continuation path. +/// tests' correctness oracle, computed host-side before the guest runs and +/// checked against its committed attestation in [`run_profile_from`]. struct ExpectedAttestation { id: [u8; 32], output: Vec, @@ -206,13 +201,9 @@ fn setup_guest_run( } /// [`setup_guest_run`]'s fixture-based counterpart for a real ethrex block: -/// reads a pre-proved continuation input instead of proving one in-process. -/// Proving a real block's continuation bundle is minutes of real prover -/// work — the `recursion-profile-block-input` Makefile target does it ONCE -/// into `executor/program_artifacts/recursion/recursion-cont-blowup4-block4.bin` -/// (+ `.expected` sidecar: 32-byte id || inner public output, written by -/// `test_dump_recursion_input`), so this test only ever measures the verifier -/// guest, never the inner prove. +/// reads a pre-proved continuation input (`make recursion-profile-block-input`) +/// instead of proving one in-process, so this test only ever measures the +/// verifier guest, never the inner prove. fn setup_block4_blowup4_guest_run() -> ( Vec, executor::elf::Elf, @@ -413,9 +404,8 @@ fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { ); } -/// Shared profiling loop for [`run_profile`]/`test_recursion_profile_blowup4_block`: runs -/// an already-set-up guest executor and prints the same cycle/step/function -/// breakdown regardless of which inner program it verified. +/// Shared profiling loop: runs an already-set-up guest executor and prints +/// the same cycle/step/function breakdown regardless of the inner program. fn run_profile_from( preset: Preset, guest_elf_bytes: &[u8], @@ -481,11 +471,8 @@ fn run_profile_from( }, ); - // Correctness, not just crash-freedom: read the guest's actual committed - // attestation and check it against the trusted host recompute - // (`expected`, built by `setup_guest_run`/`setup_block4_blowup4_guest_run` - // before the guest ran) — the same identity+output binding a production - // consumer checks via `check_attestation`/its continuation analog. + // Correctness, not just crash-freedom: check the guest's committed + // attestation against the trusted host recompute (`expected`). let committed = executor .finish() .expect("read committed output after execution") @@ -896,17 +883,12 @@ fn test_recursion_prove_1query() { /// /// Env knobs: /// * `RECURSION_DUMP_PRESET` (`min`|`blowup2`|`blowup4`|`blowup8`, default -/// `min`) — the [`Preset`] whose options the inner proof is generated under; -/// must match the `recursion-.elf` the blob will be fed to, or the -/// guest rejects the proof. -/// * `RECURSION_DUMP_INNER_ELF` (path, default the `empty` guest) — the inner -/// program to prove, for realistic trace heights (e.g. the ethrex guest). -/// * `RECURSION_DUMP_INNER_INPUT` (path, default none) — the inner program's -/// private input (e.g. an ethrex-fixtures block). -/// * `RECURSION_DUMP_EPOCH_LOG2` (int, default unset = monolithic) — prove the -/// inner via continuations with `2^n`-cycle epochs (memory-bounded prover) -/// and encode a [`recursion::ContinuationGuestInput`] blob instead; feed it -/// to `recursion-cont-.elf`, not the monolithic guest. +/// `min`) — must match the `recursion-.elf` the blob is fed to. +/// * `RECURSION_DUMP_INNER_ELF` (path, default the `empty` guest). +/// * `RECURSION_DUMP_INNER_INPUT` (path, default none). +/// * `RECURSION_DUMP_EPOCH_LOG2` (int, default unset = monolithic) — prove via +/// continuations with `2^n`-cycle epochs and encode a +/// [`recursion::ContinuationGuestInput`] blob for `recursion-cont-.elf`. #[test] #[ignore = "diagnostic: writes recursion private input to /tmp/recursion_input.bin"] fn test_dump_recursion_input() { @@ -937,15 +919,13 @@ fn test_dump_recursion_input() { }; // Continuation dumps also get an `.expected` sidecar (32-byte id || inner - // public output) — the ground truth a caller needs to check a guest's - // committed attestation, computed here while the `ContinuationProof` - // bundle still exists (`encode_continuation_guest_input` consumes it). - // Lets a consumer (e.g. `test_recursion_profile_blowup4_block`) verify - // the pre-proved fixture without re-deriving it from the bundle. + // public output), computed here while the `ContinuationProof` bundle + // still exists (`encode_continuation_guest_input` consumes it) — lets a + // consumer check the pre-proved fixture without re-deriving it. let (blob, expected_sidecar) = match std::env::var("RECURSION_DUMP_EPOCH_LOG2") { Ok(s) => { - // No recursion-cont-blowup8.elf is built (RECURSION_CONT_PRESETS stops - // at blowup4), so this blob would have no guest to verify it. + // No recursion-cont-blowup8.elf is built (RECURSION_CONT_PRESETS + // stops at blowup4). assert_ne!( preset, Preset::Blowup8, @@ -1055,14 +1035,9 @@ fn test_recursion_cycles_blowup4() { } /// Full profile (top-25 + per-step) of the recursion `continuation` guest -/// verifying a REAL ethrex block (4 transfers) — not the `empty`-program -/// diagnostic floor `test_recursion_profile_1query`/`_multiquery` measure. -/// blowup=4 (110 queries): the cheaper of the two realistic base-layer -/// presets, so the full histogram stays tractable; `bench_recursion_cycles.sh`/ -/// `bench_recursion_scaling.sh` already cover cycle counts across every preset -/// and block size, so this only needs to exist for the detailed breakdown. -/// Requires `make recursion-profile-block-input` — the pre-proved fixture -/// [`setup_block4_blowup4_guest_run`] reads. +/// verifying a REAL ethrex block (4 transfers), blowup=4 — not the +/// `empty`-program diagnostic floor `test_recursion_profile_1query`/ +/// `_multiquery` measure. Requires `make recursion-profile-block-input`. #[test] #[ignore = "diagnostic: heavy; recursion guest histogram + steps over a real ethrex block (blowup=4)"] fn test_recursion_profile_blowup4_block() { diff --git a/scripts/bench_recursion_cycles.sh b/scripts/bench_recursion_cycles.sh index 2235c9158..6a7179066 100755 --- a/scripts/bench_recursion_cycles.sh +++ b/scripts/bench_recursion_cycles.sh @@ -36,21 +36,20 @@ # REF_A ref/SHA to evaluate (the PR side). # REF_B baseline ref/SHA (default origin/main). # PRESET recursion-verifier preset (default min): min = blowup=2, 1 query -# (cheap diagnostic regime); blowup2 = blowup=2, 219 queries (the -# realistic base-layer proof shape at 128-bit — production pipelines -# prove the base at low blowup and wrap at high blowup); blowup4 = -# blowup=4, 110 queries (the other realistic base-layer point); -# blowup8 = blowup=8, 73 queries. The preset picks BOTH the guest ELF -# (recursion-.elf, falling back to recursion.elf on older refs -# that build a single unnamed min guest) AND the inner-proof options of -# the dumped blob (exported as RECURSION_DUMP_PRESET to the dump test). -# Refs predating the preset-aware dump test always dump min options, so -# only PRESET=min is measurable for them — the script fails loudly -# up front rather than let the guest reject the blob in-VM. It is -# EXPECTED and correct for the two sides to use DIFFERENT artifact -# names (e.g. main→recursion.elf vs PR→recursion-min.elf) — both are -# verified under the SAME preset options. The printed per-ref -# `guest=` labels show which each side used. +# (cheap diagnostic); blowup2 = blowup=2, 219 queries (realistic +# base-layer, 128-bit); blowup4 = blowup=4, 110 queries (the other +# base-layer point); blowup8 = blowup=8, 73 queries. Picks BOTH the +# guest ELF (recursion-.elf, falling back to recursion.elf +# on older refs) AND the dumped blob's inner-proof options (via +# RECURSION_DUMP_PRESET). Refs predating the preset-aware dump test +# only support PRESET=min — the script fails loudly up front rather +# than let the guest reject the blob in-VM. Different artifact +# names across refs (e.g. recursion.elf vs recursion-min.elf) is +# expected — both verify under the SAME preset options. +# `blowup4-block` isn't a build preset: it's the `continuation` guest +# (recursion-cont-blowup4.elf) verifying a real ethrex block instead +# of the `empty` diagnostic program — real prover minutes per ref +# (see the blob cache below), not seconds. # Env: # REBUILD=1 force rebuild of MEASURE_CLI and re-run of every ref # (guest build + blob dump + measurement); ignore caches. @@ -63,6 +62,9 @@ # PRUNE_KEEP= cap on cached ref worktrees kept under $WORK (default 10); # older ones (+ their results/blobs/logs) are pruned at startup # to bound disk on the long-lived bench runner. +# BLOCK_TXS=4 PRESET=blowup4-block only: ethrex block size, reading +# executor/tests/ethrex_bench_.bin (only _4 committed). +# BLOCK_EPOCH_LOG2=21 PRESET=blowup4-block only: inner continuation epoch size. # # Caching: each ref's result is cached in $WORK keyed on its resolved SHA + preset + the # MEASURE_CLI source SHA (so a baseline and PR side are never compared across two @@ -70,7 +72,9 @@ # read: a truncated/partial cache is discarded and re-measured, never emitted as zeros. # Ref worktrees are kept (named by SHA) so a re-measure is a cargo no-op; the newest # PRUNE_KEEP are retained and older ones pruned. A worktree whose guest build fails -# mid-run is removed immediately. REBUILD=1 forces everything. +# mid-run is removed immediately. The dumped input blob is also cached (keyed on SHA + +# preset), so re-proving blowup4-block's real ethrex block only happens once per ref. +# REBUILD=1 forces everything. # set -euo pipefail @@ -176,11 +180,34 @@ valid_result() { measure_ref() { local ref="$1" sha="$2" role="$3" local sha8="${sha:0:8}" + # `blowup4-block`: same cache/worktree/measure plumbing, but a real ethrex + # block through the continuation guest instead of the `min`/`blowup*` + # presets' `empty`-program blob. BLOCK_PRESET is the underlying build + # preset (blowup4); BLOCK_TXS/BLOCK_EPOCH_LOG2 pin the fixture and epoch + # size to what `make recursion-profile-block-input` proves. + local is_block=0 block_preset="" + if [ "$PRESET" = "blowup4-block" ]; then + is_block=1 + block_preset="blowup4" + fi + local block_txs="${BLOCK_TXS:-4}" + local block_epoch_log2="${BLOCK_EPOCH_LOG2:-21}" # Key the cache on ref SHA + preset AND the MEASURE_CLI source SHA, so a baseline and # PR side measured by different counters (after a cli change) never share a result. local result="$WORK/result_${sha8}_${PRESET}_m${HEAD_SHA:0:8}.txt" local wt="$WORK/wt_${sha8}" + # Blob cache: keyed on sha + preset (+ block fixture/epoch), persists across runs. + local blob_key="$PRESET" + if [ "$is_block" = 1 ]; then + blob_key="${PRESET}_txs${block_txs}_epoch${block_epoch_log2}" + fi + local blob="$WORK/blob_${sha8}_${blob_key}.bin" + local need_dump=1 + if [ "${REBUILD:-0}" != "1" ] && [ -s "$blob" ]; then + need_dump=0 + fi + if [ "${REBUILD:-0}" != "1" ] && [ -f "$result" ]; then if valid_result < "$result"; then echo "==> [$role] Reusing cached measurement: $ref ($sha8) preset=$PRESET" >&2 @@ -204,16 +231,21 @@ measure_ref() { fi touch "$wt" 2>/dev/null || true - # 2a. Build the recursion guest ELF(s) (+ empty.elf inner program). GUEST_TARGET_DIR, - # when set, shares the RV64 build dir across ref worktrees (reuses build-std). + # 2a. Build the recursion guest ELF(s) (+ empty.elf inner program), and for + # block mode also the ethrex inner guest. GUEST_TARGET_DIR, when set, shares + # the RV64 build dir across ref worktrees (reuses build-std). echo "==> [$role] make compile-recursion-elfs @ $sha8 (slow the first time) ..." >&2 local glog="$WORK/build_guest_${sha8}.log" - local -a make_args=(compile-recursion-elfs) + local -a make_goals=(compile-recursion-elfs) + if [ "$is_block" = 1 ] && [ "$need_dump" = 1 ]; then + make_goals+=(executor/program_artifacts/rust/ethrex.elf) + fi + local -a make_args=("${make_goals[@]}") if [ -n "${GUEST_TARGET_DIR:-}" ]; then make_args+=("SHARED_TARGET_DIR=$GUEST_TARGET_DIR") fi if ! ( cd "$wt" && SYSROOT_DIR="$SYSROOT_DIR" make "${make_args[@]}" ) >"$glog" 2>&1; then - echo "ERROR: [$role] 'make compile-recursion-elfs' failed for $ref ($sha8). Tail of $glog:" >&2 + echo "ERROR: [$role] 'make ${make_goals[*]}' failed for $ref ($sha8). Tail of $glog:" >&2 tail -40 "$glog" >&2 # A failed build can leave a partial worktree; drop it so it never lingers or # poisons a later reuse. (The startup prune also caps total worktrees.) @@ -222,10 +254,17 @@ measure_ref() { exit 1 fi - # 2b. Detect the guest ELF: prefer recursion-.elf, else recursion.elf. + # 2b. Detect the guest ELF: block mode always wants recursion-cont-.elf; + # otherwise prefer recursion-.elf, else recursion.elf. local artdir="$wt/executor/program_artifacts/recursion" local guest_elf="" - if [ -f "$artdir/recursion-${PRESET}.elf" ]; then + if [ "$is_block" = 1 ]; then + guest_elf="$artdir/recursion-cont-${block_preset}.elf" + if [ ! -f "$guest_elf" ]; then + echo "ERROR: [$role] no $guest_elf for $ref ($sha8) — ref predates the continuation guest." >&2 + exit 1 + fi + elif [ -f "$artdir/recursion-${PRESET}.elf" ]; then guest_elf="$artdir/recursion-${PRESET}.elf" elif [ -f "$artdir/recursion.elf" ]; then guest_elf="$artdir/recursion.elf" @@ -237,40 +276,59 @@ measure_ref() { fi echo "==> [$role] guest ELF: $(basename "$guest_elf")" >&2 - # 2c. Generate this ref's own input blob via its ignored dump test, under this - # preset's options (RECURSION_DUMP_PRESET). Refs predating the preset-aware dump - # test always prove the min options; feeding that blob to a non-min guest would - # only fail in-VM verification much later, so refuse loudly up front instead. - if ! grep -rq "fn test_dump_recursion_input" "$wt/prover/src/tests/" 2>/dev/null; then - echo "ERROR: [$role] ref $ref ($sha8) has no 'test_dump_recursion_input' — cannot generate its input blob." >&2 - exit 1 - fi - if [ "$PRESET" != "min" ] && ! grep -rq "RECURSION_DUMP_PRESET" "$wt/prover/src/tests/" 2>/dev/null; then - echo "ERROR: [$role] ref $ref ($sha8) predates the preset-aware dump test (no RECURSION_DUMP_PRESET) — only PRESET=min is measurable for it." >&2 - exit 1 - fi - echo "==> [$role] dumping recursion input blob (cargo test test_dump_recursion_input, preset=$PRESET) ..." >&2 - rm -f /tmp/recursion_input.bin - local dlog="$WORK/dump_${sha8}_${PRESET}.log" - if [ -n "${HOST_TARGET_DIR:-}" ]; then - if ! ( cd "$wt" && RECURSION_DUMP_PRESET="$PRESET" CARGO_TARGET_DIR="$HOST_TARGET_DIR" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then - echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 - tail -40 "$dlog" >&2 + # 2c. Generate this ref's own input blob via its ignored dump test, unless a + # cached blob covers this sha/preset already (need_dump=0). Refuse up front if + # the ref predates a needed knob, instead of failing in-VM verification later. + if [ "$need_dump" = 0 ]; then + echo "==> [$role] Reusing cached recursion input blob ($blob) — skipping re-prove." >&2 + else + if ! grep -rq "fn test_dump_recursion_input" "$wt/prover/src/tests/" 2>/dev/null; then + echo "ERROR: [$role] ref $ref ($sha8) has no 'test_dump_recursion_input' — cannot generate its input blob." >&2 exit 1 fi - else - if ! ( cd "$wt" && RECURSION_DUMP_PRESET="$PRESET" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then - echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 - tail -40 "$dlog" >&2 + if [ "$PRESET" != "min" ] && ! grep -rq "RECURSION_DUMP_PRESET" "$wt/prover/src/tests/" 2>/dev/null; then + echo "ERROR: [$role] ref $ref ($sha8) predates the preset-aware dump test (no RECURSION_DUMP_PRESET) — only PRESET=min is measurable for it." >&2 exit 1 fi + local -a dump_env=("RECURSION_DUMP_PRESET=${block_preset:-$PRESET}") + if [ "$is_block" = 1 ]; then + if ! grep -rq "RECURSION_DUMP_EPOCH_LOG2" "$wt/prover/src/tests/" 2>/dev/null; then + echo "ERROR: [$role] ref $ref ($sha8) predates RECURSION_DUMP_EPOCH_LOG2 — blowup4-block is not measurable for it." >&2 + exit 1 + fi + local block_fixture="$wt/executor/tests/ethrex_bench_${block_txs}.bin" + if [ ! -f "$block_fixture" ]; then + echo "ERROR: [$role] ref $ref ($sha8) is missing $block_fixture (ethrex block fixture) — blowup4-block is not measurable for it." >&2 + exit 1 + fi + dump_env+=( + "RECURSION_DUMP_EPOCH_LOG2=$block_epoch_log2" + "RECURSION_DUMP_INNER_ELF=$wt/executor/program_artifacts/rust/ethrex.elf" + "RECURSION_DUMP_INNER_INPUT=$block_fixture" + ) + fi + echo "==> [$role] dumping recursion input blob (cargo test test_dump_recursion_input, preset=$PRESET) ..." >&2 + rm -f /tmp/recursion_input.bin + local dlog="$WORK/dump_${sha8}_${PRESET}.log" + if [ -n "${HOST_TARGET_DIR:-}" ]; then + if ! ( cd "$wt" && env "${dump_env[@]}" CARGO_TARGET_DIR="$HOST_TARGET_DIR" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then + echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 + tail -40 "$dlog" >&2 + exit 1 + fi + else + if ! ( cd "$wt" && env "${dump_env[@]}" cargo test --release -p lambda-vm-prover --lib test_dump_recursion_input -- --ignored --nocapture ) >"$dlog" 2>&1; then + echo "ERROR: [$role] blob-dump test failed for $ref ($sha8). Tail of $dlog:" >&2 + tail -40 "$dlog" >&2 + exit 1 + fi + fi + if [ ! -f /tmp/recursion_input.bin ]; then + echo "ERROR: [$role] test_dump_recursion_input did not write /tmp/recursion_input.bin for $ref ($sha8)." >&2 + exit 1 + fi + mv /tmp/recursion_input.bin "$blob" fi - if [ ! -f /tmp/recursion_input.bin ]; then - echo "ERROR: [$role] test_dump_recursion_input did not write /tmp/recursion_input.bin for $ref ($sha8)." >&2 - exit 1 - fi - local blob="$WORK/blob_${sha8}_${PRESET}.bin" - cp /tmp/recursion_input.bin "$blob" echo "==> [$role] blob: $(wc -c <"$blob" | tr -d '[:space:]') bytes -> $blob" >&2 # 2d. Measure: one deterministic execute --cycles run. Time it (CI feasibility). @@ -356,6 +414,7 @@ case "$PRESET" in blowup2) REGIME="128-bit (blowup=2, 219 queries — realistic base-layer)" ;; blowup4) REGIME="128-bit (blowup=4, 110 queries — realistic base-layer)" ;; blowup8) REGIME="128-bit (blowup=8, 73 queries)" ;; + blowup4-block) REGIME="128-bit (blowup=4, 110 queries) — real ethrex block, 4 transfers" ;; *) REGIME="$PRESET" ;; esac diff --git a/scripts/bench_recursion_scaling.sh b/scripts/bench_recursion_scaling.sh index 11a3d67f5..c868586db 100755 --- a/scripts/bench_recursion_scaling.sh +++ b/scripts/bench_recursion_scaling.sh @@ -6,13 +6,11 @@ # execution via CONTINUATIONS (memory-bounded 2^EPOCH_LOG2-cycle epochs, so any # block size proves on a bounded-RAM box), then executes the continuation # recursion guest (recursion-cont-.elf) on the bundle and records the -# EXACT deterministic guest cycle count — the in-VM cost of verifying that -# block's proof. +# exact deterministic guest cycle count. # -# Sweep order is PRESET-MAJOR on purpose: the full block-size curve for the -# first preset completes before the next preset starts, so the headline regime -# (blowup2 = the realistic base-layer options, 219 FRI queries) yields a usable -# scaling curve as early as possible instead of only when everything ends. +# Sweep order is PRESET-MAJOR: the full block-size curve for the first preset +# completes before the next starts, so the headline regime (blowup2) yields a +# usable curve early instead of only when everything ends. # # Usage: scripts/bench_recursion_scaling.sh [RESULTS_FILE=/tmp/recursion_scaling.txt] # Env: From 2e8e24aa578d9660bfa539e64d59bd6ac16c3900 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 16:03:55 -0300 Subject: [PATCH 08/13] extract bench-verify.yml's recursion preset loop into a script Moves the inline shell for looping presets (min/blowup2/blowup4/ blowup4-block) into .github/scripts/run_recursion_bench.sh, matching the repo's other .github/scripts/*.sh conventions. --- .github/scripts/run_recursion_bench.sh | 44 ++++++++++++++++++++++++++ .github/workflows/bench-verify.yml | 29 +---------------- 2 files changed, 45 insertions(+), 28 deletions(-) create mode 100755 .github/scripts/run_recursion_bench.sh diff --git a/.github/scripts/run_recursion_bench.sh b/.github/scripts/run_recursion_bench.sh new file mode 100755 index 000000000..ce3b98b6c --- /dev/null +++ b/.github/scripts/run_recursion_bench.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Runs scripts/bench_recursion_cycles.sh across every preset regime (min, +# blowup2/blowup4, blowup4-block) and appends each result — or an +# "unavailable" note when the ref/preset combo isn't supported — to +# /tmp/recursion_result.txt for the bench-verify.yml PR comment. +# +# Usage: .github/scripts/run_recursion_bench.sh HEAD_SHA +set -euo pipefail + +HEAD_SHA="$1" +RESULT=/tmp/recursion_result.txt +: > "$RESULT" + +run_preset() { + local preset="$1" log="/tmp/recursion_out_${preset}.txt" + if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "$log"; then + { echo; sed -n '/=== Recursion-guest cycle/,$p' "$log"; } >> "$RESULT" + else + { echo; echo "_(${preset} regime unavailable for these refs — see the workflow log.)_"; } >> "$RESULT" + fi +} + +run_preset min +# Post-result's raw-log fallback reads /tmp/recursion_out.txt (unsuffixed). +cp -f /tmp/recursion_out_min.txt /tmp/recursion_out.txt + +# blowup2/blowup4: full-query base-layer regimes over the `empty` diagnostic +# program. Need origin/main's RECURSION_DUMP_PRESET support to dump a +# non-min blob; checked once up front instead of failing each preset in turn. +if git grep -q RECURSION_DUMP_PRESET origin/main -- prover/src/tests/ 2>/dev/null; then + run_preset blowup2 + run_preset blowup4 +else + { echo; echo "_(blowup2/blowup4 full-query regimes need \`origin/main\` to have the preset-aware dump test (RECURSION_DUMP_PRESET) — not merged yet, so only \`min\` is compared for this PR.)_"; } >> "$RESULT" +fi + +# blowup4-block: same blowup=4 verifier over a REAL ethrex block (via the +# `continuation` guest). Needs origin/main's RECURSION_DUMP_EPOCH_LOG2 support. +if git grep -q RECURSION_DUMP_EPOCH_LOG2 origin/main -- prover/src/tests/ 2>/dev/null; then + run_preset blowup4-block +else + { echo; echo "_(blowup4-block real-ethrex-block regime needs \`origin/main\` to support RECURSION_DUMP_EPOCH_LOG2 — not merged yet.)_"; } >> "$RESULT" +fi diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index 9969e12f6..9e6d92422 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -111,34 +111,7 @@ jobs: HOST_TARGET_DIR: /tmp/recursion_cycles_run/shared_host_target run: | export SYSROOT_DIR="$HOME/.lambda-vm-sysroot" - set -o pipefail - scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main min 2>&1 | tee /tmp/recursion_out.txt - sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out.txt > /tmp/recursion_result.txt - # Full-query regimes, reusing the min run's cached guest builds/worktrees. - # origin/main needs RECURSION_DUMP_PRESET support to dump non-min blobs; - # check once up front instead of failing each preset in turn. - if git grep -q RECURSION_DUMP_PRESET origin/main -- prover/src/tests/ 2>/dev/null; then - for preset in blowup2 blowup4; do - if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then - { echo; sed -n '/=== Recursion-guest cycle/,$p' "/tmp/recursion_out_${preset}.txt"; } >> /tmp/recursion_result.txt - else - { echo; echo "_(${preset} full-query regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt - fi - done - else - { echo; echo "_(blowup2/blowup4 full-query regimes need \`origin/main\` to have the preset-aware dump test (RECURSION_DUMP_PRESET) — not merged yet, so only \`min\` is compared for this PR.)_"; } >> /tmp/recursion_result.txt - fi - # Real-block regime: needs origin/main to support RECURSION_DUMP_EPOCH_LOG2 - # (continuation dumps); checked once up front like the loop above. - if git grep -q RECURSION_DUMP_EPOCH_LOG2 origin/main -- prover/src/tests/ 2>/dev/null; then - if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main blowup4-block 2>&1 | tee /tmp/recursion_out_blowup4-block.txt; then - { echo; sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out_blowup4-block.txt; } >> /tmp/recursion_result.txt - else - { echo; echo "_(blowup4-block real-ethrex-block regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt - fi - else - { echo; echo "_(blowup4-block real-ethrex-block regime needs \`origin/main\` to support RECURSION_DUMP_EPOCH_LOG2 — not merged yet.)_"; } >> /tmp/recursion_result.txt - fi + .github/scripts/run_recursion_bench.sh "$HEAD_SHA" - name: Post result if: always() From c4f5e1a9a366ac2a05e1655444032f9c8b0a54a2 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 17:20:04 -0300 Subject: [PATCH 09/13] bench-verify.yml: add workflow_dispatch to test branch changes directly --- .github/workflows/bench-verify.yml | 42 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index 9e6d92422..313b9f378 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -1,7 +1,15 @@ name: Bench verifier -# Manual-only (/bench-verify); separate from /bench so they never share the bench server. +# Manual-only (/bench-verify, or workflow_dispatch to test workflow changes on a +# branch — issue_comment always runs main's copy of this file, see profile-recursion.yml); +# separate from /bench so they never share the bench server. on: + workflow_dispatch: + inputs: + pairs: + description: "ABBA pair count (2-40)" + required: false + default: "20" issue_comment: types: [created] @@ -22,9 +30,11 @@ permissions: jobs: verify: if: >- - github.event.issue.pull_request && - startsWith(github.event.comment.body, '/bench-verify') && - contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/bench-verify') && + contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)) runs-on: [self-hosted, bench] # Job cap. On a cold runner the recursion BUILDS dominate: MEASURE_CLI once, plus # per ref a guest build and a prover-test build. Cached in /tmp; build-std / host @@ -32,6 +42,7 @@ jobs: timeout-minutes: 90 steps: - name: Acknowledge (react + occupancy notice) + if: github.event_name == 'issue_comment' uses: actions/github-script@v7 with: script: | @@ -51,13 +62,20 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUM: ${{ github.event.issue.number }} COMMENT_BODY: ${{ github.event.comment.body }} + DISPATCH_PAIRS: ${{ github.event.inputs.pairs }} run: | - # Head SHA (not branch name) so fork PRs resolve and a mid-run force-push can't race. - HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid) + if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then + # Testing this workflow's own changes: bench the dispatched branch vs main. + HEAD_SHA="$GITHUB_SHA" + N="${DISPATCH_PAIRS:-20}" + else + # Head SHA (not branch name) so fork PRs resolve and a mid-run force-push can't race. + HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid) + # Optional pair count "/bench-verify 32"; default 20. + N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench-verify[[:space:]]*\([0-9]\+\).*|\1|p') + N=${N:-20} + fi echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" - # Optional pair count "/bench-verify 32"; default 20, clamp [2,40]. - N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench-verify[[:space:]]*\([0-9]\+\).*|\1|p') - N=${N:-20} if [ "$N" -lt 2 ] 2>/dev/null || [ "$N" -gt 40 ] 2>/dev/null; then echo "::warning::pair count $N out of range [2,40]; using 20" N=20 @@ -70,6 +88,7 @@ jobs: fetch-depth: 0 - name: Fetch PR head commit (works for fork PRs) + if: github.event_name == 'issue_comment' env: PR_NUM: ${{ github.event.issue.number }} run: git fetch origin "pull/$PR_NUM/head" --quiet @@ -153,6 +172,11 @@ jobs: body += '⚠️ Recursion cycle bench did not complete (does not affect the verifier verdict above).'; body += rtail ? ' Last log lines:\n\n' + '```\n' + rtail + '\n```\n' : '\n'; } + // workflow_dispatch has no PR to comment on; write to the job summary instead. + if (context.eventName !== 'issue_comment') { + await core.summary.addRaw(body).write(); + return; + } const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, From 0a06910b66514103fff329cb802de1b12d68c6e9 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 17:39:47 -0300 Subject: [PATCH 10/13] fix(scripts): unbound-variable crash in run_recursion_bench.sh local a=x b=$a expands $a before the assignment lands, so under set -u this failed as "preset: unbound variable". Split into two local statements. --- .github/scripts/run_recursion_bench.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/run_recursion_bench.sh b/.github/scripts/run_recursion_bench.sh index ce3b98b6c..05e7f5b1d 100755 --- a/.github/scripts/run_recursion_bench.sh +++ b/.github/scripts/run_recursion_bench.sh @@ -13,7 +13,8 @@ RESULT=/tmp/recursion_result.txt : > "$RESULT" run_preset() { - local preset="$1" log="/tmp/recursion_out_${preset}.txt" + local preset="$1" + local log="/tmp/recursion_out_${preset}.txt" if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "$log"; then { echo; sed -n '/=== Recursion-guest cycle/,$p' "$log"; } >> "$RESULT" else From 1841931621d8cdaa59202a0a5585b4cfe09c2fcb Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 17:46:39 -0300 Subject: [PATCH 11/13] scripts: stop truncating function names in the recursion profile table Cut generic-monomorphized Rust symbol names to 90 chars, hiding the type params that usually matter most when reading the profile. --- .github/scripts/aggregate_recursion_histogram.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/scripts/aggregate_recursion_histogram.py b/.github/scripts/aggregate_recursion_histogram.py index 0be0a3010..2092c05b0 100755 --- a/.github/scripts/aggregate_recursion_histogram.py +++ b/.github/scripts/aggregate_recursion_histogram.py @@ -93,10 +93,6 @@ def parse(text): return total_cycles, unique_pcs, exec_time, tables -def short(name, width=90): - return name if len(name) <= width else name[: width - 1] + "…" - - def render_table(rows, denom_label): if not rows: return "> _no rows_\n" @@ -105,7 +101,7 @@ def render_table(rows, denom_label): for i, r in enumerate(rows, 1): body += ( f"| {i} | {r['cycles']:,} | {r['pct']}% | {r['cum']}% | " - f"{r['pcs']} | `{short(r['fn'])}` |\n" + f"{r['pcs']} | `{r['fn']}` |\n" ) last_cum = rows[-1]["cum"] body += ( From 710c74aad2729b1759479d813b7c3f0ff7f61e29 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 17:54:57 -0300 Subject: [PATCH 12/13] bench-verify.yml: fail the job if the recursion cycle bench didn't complete continue-on-error on that step exists to protect the already-posted verifier result, not to hide a broken recursion step behind a green job. Add a final step, after the result posts, that fails the job on a non-success recursion outcome. --- .github/workflows/bench-verify.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index 313b9f378..ad07db1fe 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -194,3 +194,13 @@ jobs: issue_number: context.issue.number, body }); } + + # The recursion step's continue-on-error exists so its failure never costs the + # already-captured/posted verifier result — not to hide the failure. Fail here, + # after that result is safely posted, so the job's overall status still reflects + # reality instead of reading as a full green success. + - name: Fail if recursion cycle bench didn't complete + if: always() && steps.recursion.outcome != 'success' + run: | + echo "::error::Recursion cycle bench step did not complete (outcome=${{ steps.recursion.outcome }}) — see its log and the posted result above." + exit 1 From 891a5ec603899e3a7e7cbd5128523819180cbbca Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Mon, 20 Jul 2026 18:11:54 -0300 Subject: [PATCH 13/13] fix review findings: stale block-preset result cache, unvalidated pairs input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Result cache was keyed on $PRESET alone while the blob cache also folds in BLOCK_TXS/BLOCK_EPOCH_LOG2, so overriding those on a cached ref silently reused a stale measurement. Also validate the workflow_dispatch pairs input is numeric before the range check — a non-numeric value made both comparisons error out and pass through unchanged. --- .github/workflows/bench-verify.yml | 11 ++++++----- scripts/bench_recursion_cycles.sh | 10 ++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bench-verify.yml b/.github/workflows/bench-verify.yml index ad07db1fe..0641195cc 100644 --- a/.github/workflows/bench-verify.yml +++ b/.github/workflows/bench-verify.yml @@ -76,7 +76,11 @@ jobs: N=${N:-20} fi echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" - if [ "$N" -lt 2 ] 2>/dev/null || [ "$N" -gt 40 ] 2>/dev/null; then + if ! [[ "$N" =~ ^[0-9]+$ ]]; then + echo "::warning::pair count '$N' is not a number; using 20" + N=20 + fi + if [ "$N" -lt 2 ] || [ "$N" -gt 40 ]; then echo "::warning::pair count $N out of range [2,40]; using 20" N=20 fi @@ -195,10 +199,7 @@ jobs: }); } - # The recursion step's continue-on-error exists so its failure never costs the - # already-captured/posted verifier result — not to hide the failure. Fail here, - # after that result is safely posted, so the job's overall status still reflects - # reality instead of reading as a full green success. + # continue-on-error above protects the posted verifier result, not the failure itself. - name: Fail if recursion cycle bench didn't complete if: always() && steps.recursion.outcome != 'success' run: | diff --git a/scripts/bench_recursion_cycles.sh b/scripts/bench_recursion_cycles.sh index 6a7179066..c4bc06461 100755 --- a/scripts/bench_recursion_cycles.sh +++ b/scripts/bench_recursion_cycles.sh @@ -192,16 +192,18 @@ measure_ref() { fi local block_txs="${BLOCK_TXS:-4}" local block_epoch_log2="${BLOCK_EPOCH_LOG2:-21}" - # Key the cache on ref SHA + preset AND the MEASURE_CLI source SHA, so a baseline and - # PR side measured by different counters (after a cli change) never share a result. - local result="$WORK/result_${sha8}_${PRESET}_m${HEAD_SHA:0:8}.txt" - local wt="$WORK/wt_${sha8}" # Blob cache: keyed on sha + preset (+ block fixture/epoch), persists across runs. local blob_key="$PRESET" if [ "$is_block" = 1 ]; then blob_key="${PRESET}_txs${block_txs}_epoch${block_epoch_log2}" fi + # Key the result cache on ref SHA + blob_key (so a BLOCK_TXS/BLOCK_EPOCH_LOG2 + # override never reuses a stale measurement) AND the MEASURE_CLI source SHA + # (so a baseline and PR side measured by different counters never share a result). + local result="$WORK/result_${sha8}_${blob_key}_m${HEAD_SHA:0:8}.txt" + local wt="$WORK/wt_${sha8}" + local blob="$WORK/blob_${sha8}_${blob_key}.bin" local need_dump=1 if [ "${REBUILD:-0}" != "1" ] && [ -s "$blob" ]; then