From 2678339eedcdf3e49a4aab8ed0f2276638b32730 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Wed, 15 Jul 2026 16:34:27 -0300 Subject: [PATCH 1/2] 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. - A 'continuation' guest feature: verify a whole ContinuationProof bundle in-VM (recursion-cont-.elf) instead of a monolithic VmProof, so the inner prove is memory-bounded via epochs at any block size. The bundle rides the same LVMR-prefixed rkyv wire format as the monolithic input (ContinuationGuestInput + verify_continuation_and_attest_blob: bytecheck + one rkyv::deserialize pass; zero-copy epoch verify over the archived bundle is follow-up). - verify_continuation_with_roots + continuation_precomputed_commitments: supplied DECODE/page-genesis roots skip the in-VM FFT+Merkle recomputes; binding stays with the attestation fold + consumer recompute, exactly like the monolithic guest path. 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 (fails loudly on refs that predate it), --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), preset-major so the headline curve completes first; DUMP_FEATURES=cuda for GPU boxes. - MAX_PRIVATE_INPUT_SIZE 64 MiB -> 512 MiB: real proofs outgrew the cap the constant was sized for (ethrex_20 at production options is ~231 MiB monolithic; continuation bundles carry one proof per epoch). Nothing is mapped above the region until the stack, so the layout is unaffected. Measured (deterministic guest cycles, epoch=2^21, blowup2): verifying an ethrex block costs ~8B cycles/epoch pre-rkyv and ~5.6B post-rkyv (#769, -30%), ~98% of it in the 219 queries' openings - naive in-VM recursion expands work ~2,700-3,800x per layer. --- .github/workflows/bench-verify.yml | 22 ++- Makefile | 27 +++- bench_vs/lambda/recursion/Cargo.toml | 37 ++++- bench_vs/lambda/recursion/src/main.rs | 57 ++++++-- executor/.gitignore | 6 + executor/src/vm/memory.rs | 14 +- 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 executor/tests/flamegraph.rs | 2 +- prover/src/continuation.rs | 97 ++++++++++++-- prover/src/recursion.rs | 152 ++++++++++++++++++++- prover/src/tests/recursion_smoke_test.rs | 163 ++++++++++++++++++++++- scripts/bench_recursion_cycles.sh | 60 ++++++--- scripts/bench_recursion_scaling.sh | 112 ++++++++++++++++ 16 files changed, 679 insertions(+), 70 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 110c2d31f..440bfaec1 100644 --- a/Makefile +++ b/Makefile @@ -56,13 +56,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 +220,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 +241,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) 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/src/vm/memory.rs b/executor/src/vm/memory.rs index ea3b06c20..fbdc914a9 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -42,10 +42,16 @@ pub type U64HashMap = HashMap; /// The COMMIT AIR concatenates calls via the running `x254` index, so this /// is enforced as a running-total budget rather than a per-call limit. pub const MAX_PUBLIC_OUTPUT_TOTAL_SIZE: u64 = 1024 * 1024; -/// Maximum size of the private input memory region (in bytes). 64 MiB so that a -/// whole `VmProof` can be passed as private input to a verifier guest (naive -/// recursion). -pub const MAX_PRIVATE_INPUT_SIZE: u64 = 64 * 1024 * 1024; +/// Maximum size of the private input memory region (in bytes). Sized so that a +/// whole `VmProof` — or a multi-epoch `ContinuationProof` bundle — can be +/// passed as private input to a verifier guest (naive recursion): at +/// production options (blowup=2, 219 FRI queries) real proofs are large — +/// ethrex with 20 transfers proves to ~231 MiB monolithic, and a continuation +/// bundle carries one such proof per epoch — so 512 MiB with the guest ELF and +/// encoding overhead riding along. Nothing else is mapped above +/// `PRIVATE_INPUT_START_INDEX` until the stack at the top of the address +/// space, so growing this region is layout-safe. +pub const MAX_PRIVATE_INPUT_SIZE: u64 = 512 * 1024 * 1024; /// Fixed high address where private input is mapped. Guest programs can read /// directly from this address (ZisK-style memory-mapped input). /// Layout: 4-byte LE length prefix at `PRIVATE_INPUT_START_INDEX`, then data at +4. 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/executor/tests/flamegraph.rs b/executor/tests/flamegraph.rs index b0c5b7a24..f5735c226 100644 --- a/executor/tests/flamegraph.rs +++ b/executor/tests/flamegraph.rs @@ -892,7 +892,7 @@ fn test_run_with_flamegraph_returns_generator_on_executor_new_failure() { // even on failure. let elf_bytes = std::fs::read("./program_artifacts/rust/add.elf").unwrap(); let program = executor::elf::Elf::load(&elf_bytes).unwrap(); - let oversized_input = vec![0u8; 64 * 1024 * 1024 + 1]; + let oversized_input = vec![0u8; executor::vm::memory::MAX_PRIVATE_INPUT_SIZE as usize + 1]; let (generator, result) = executor::flamegraph::run_with_flamegraph( &elf_bytes, diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 2e5c56a8b..d1eb047aa 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -212,6 +212,7 @@ fn l2g_memory_air( fn global_memory_air( opts: &ProofOptions, config: &PageConfig, + preprocessed: Option, ) -> AirWithBuses { let air = AirWithBuses::new( global_memory::cols::NUM_COLUMNS, @@ -225,11 +226,17 @@ fn global_memory_air( if config.is_private_input { return air; } - let commitment = if config.init_values.is_some() { - page::compute_precomputed_commitment(config, opts) - } else { - page::zero_init_preprocessed_commitment(opts) - }; + // `preprocessed` lets a caller skip the genesis recompute (an FFT + Merkle + // pass per page) by supplying the root — the recursion guest's in-VM path, + // where the binding comes from the attestation fold instead (see + // `recursion::verify_continuation_and_attest`). `None` = recompute here. + let commitment = preprocessed.unwrap_or_else(|| { + if config.init_values.is_some() { + page::compute_precomputed_commitment(config, opts) + } else { + page::zero_init_preprocessed_commitment(opts) + } + }); air.with_preprocessed(commitment, global_memory::NUM_PREPROCESSED_COLS) } @@ -404,6 +411,7 @@ impl ContinuationProof { /// INIT = `register_init` and FINI = `reg_fini`. Continuation epochs /// use the L2G bookend, so PAGE is skipped and `page_configs` is empty. The /// epoch-local L2G air is built separately by the caller (it needs the `label`). +#[allow(clippy::too_many_arguments)] fn build_epoch_airs( elf: &Elf, opts: &ProofOptions, @@ -412,6 +420,7 @@ fn build_epoch_airs( register_init: &[u32], reg_fini: &[u32], is_final: bool, + decode_commitment: Option, ) -> VmAirs { // Continuation epochs preprocess FINI = R_{i+1} too (not just INIT = R_i), so the // final register file is a verifier-known public value bound by the REG-C2 @@ -427,7 +436,7 @@ fn build_epoch_airs( false, page_configs, table_counts, - None, + decode_commitment, is_final, None, None, @@ -480,6 +489,7 @@ fn prove_epoch( start.register_init, ®_fini, is_final, + None, ); let label = start.label; @@ -536,6 +546,7 @@ fn prove_epoch( /// continuation epochs, so the AIRs are built with no page configs (the bundle does /// not get to supply any). Returns `true` iff the proof verifies and its committed /// L2G root matches the claimed one. +#[allow(clippy::too_many_arguments)] fn verify_epoch( elf: &Elf, elf_bytes: &[u8], @@ -544,6 +555,7 @@ fn verify_epoch( is_final: bool, label: u64, opts: &ProofOptions, + decode_commitment: Option, ) -> bool { // Reject degenerate table counts (mirrors the monolithic verifier). if epoch.table_counts.validate().is_err() { @@ -571,6 +583,7 @@ fn verify_epoch( register_init, &epoch.reg_fini, is_final, + decode_commitment, ); let l2g_air = l2g_memory_air(opts, label); let mut refs = airs.air_refs(); @@ -670,7 +683,7 @@ fn prove_global( .collect(); let gm_airs: Vec<_> = gm_configs .iter() - .map(|config| global_memory_air(opts, config)) + .map(|config| global_memory_air(opts, config, None)) .collect(); let mut pairs: Vec<(AirRef, &mut TraceTable, &())> = l2g_airs @@ -697,6 +710,7 @@ fn prove_global( .map_err(|e| Error::Prover(format!("{e:?}"))) } +#[allow(clippy::too_many_arguments)] fn verify_global( num_epochs: usize, page_bases: &[u64], @@ -705,6 +719,7 @@ fn verify_global( elf_bytes: &[u8], num_private_input_pages: usize, opts: &ProofOptions, + page_genesis_commitments: Option<&[(u64, Commitment)]>, ) -> bool { // One L2G air per epoch, each with its own 1-based `fini_epoch` constant — // must match the order/labels the global proof committed in `prove_global`. @@ -719,10 +734,29 @@ fn verify_global( // recomputes their genesis from the ELF; the GlobalMemory bus enforces them. A // wrong `num_private_input_pages` flips a touched page's preprocessed mode, so the // rebuilt AIR no longer matches the committed trace and `multi_verify` rejects. + // + // `page_genesis_commitments` (the recursion guest's supplied roots) skips the + // per-data-page recompute; a supplied root shifts the genesis binding to the + // attestation fold + consumer recompute, exactly like the monolithic guest's + // `page_commitments`. Zero-init pages always share one commitment, computed + // once here rather than per touched page. let gm_configs = global_memory_configs(page_bases, elf, num_private_input_pages); + let supplied: HashMap = page_genesis_commitments + .map(|s| s.iter().copied().collect()) + .unwrap_or_default(); + let zero_init_root = page::zero_init_preprocessed_commitment(opts); let gm_airs: Vec<_> = gm_configs .iter() - .map(|config| global_memory_air(opts, config)) + .map(|config| { + let preprocessed = if config.is_private_input { + None + } else if config.init_values.is_some() { + supplied.get(&config.page_base).copied() + } else { + Some(zero_init_root) + }; + global_memory_air(opts, config, preprocessed) + }) .collect(); let mut refs: Vec = l2g_airs.iter().map(|a| a as AirRef).collect(); @@ -924,6 +958,25 @@ pub fn verify_continuation( elf_bytes: &[u8], bundle: &ContinuationProof, opts: &ProofOptions, +) -> Result>, Error> { + verify_continuation_with_roots(elf_bytes, bundle, opts, None, None) +} + +/// [`verify_continuation`] with caller-supplied ELF-derived roots: the DECODE +/// preprocessed root (shared by every epoch) and the global-memory genesis +/// roots for touched data pages. Supplied roots are used VERBATIM — they are +/// NOT bound to `elf_bytes` here, exactly like `verify_with_options`' supplied +/// roots on the monolithic path. The recursion guest supplies them via private +/// input to skip the in-VM FFT + Merkle recomputes; on success it folds them +/// into the attestation's `program_id`, and the consumer's recompute+compare +/// ([`crate::recursion::check_attestation`]) is what restores the binding. +/// `None` = recompute from the ELF (the trustless host path). +pub fn verify_continuation_with_roots( + elf_bytes: &[u8], + bundle: &ContinuationProof, + opts: &ProofOptions, + decode_commitment: Option, + page_genesis_commitments: Option<&[(u64, Commitment)]>, ) -> Result>, Error> { // Bound the claimed private-input page count before using it to size/allocate AIRs // (mirrors `verify_with_options`). The count is also bound into the global proof's @@ -974,6 +1027,7 @@ pub fn verify_continuation( is_final, label, opts, + decode_commitment, ) { return Ok(None); } @@ -1019,6 +1073,7 @@ pub fn verify_continuation( elf_bytes, bundle.num_private_input_pages, opts, + page_genesis_commitments, ) { return Ok(None); } @@ -1031,6 +1086,28 @@ pub fn verify_continuation( Ok(Some(public_output)) } +/// Precompute the ELF-derived roots [`verify_continuation_with_roots`] accepts: +/// the DECODE preprocessed root and one genesis root per touched non-private +/// data page (the same set `verify_global` would rebuild from the ELF). These +/// are what the host packs into the continuation recursion guest's private +/// input, and what a consumer recomputes to re-bind the guest's attestation. +pub fn continuation_precomputed_commitments( + elf_bytes: &[u8], + bundle: &ContinuationProof, + opts: &ProofOptions, +) -> Result<(Commitment, Vec<(u64, Commitment)>), Error> { + let elf = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; + let decode_commitment = crate::tables::decode::commitment_from_elf(&elf, opts) + .map_err(|e| Error::Recursion(format!("DECODE commitment from ELF: {e}")))?; + let page_bases = canonical_page_bases(&bundle.touched_page_bases); + let page_commitments = global_memory_configs(&page_bases, &elf, bundle.num_private_input_pages) + .iter() + .filter(|c| !c.is_private_input && c.init_values.is_some()) + .map(|c| (c.page_base, page::compute_precomputed_commitment(c, opts))) + .collect(); + Ok((decode_commitment, page_commitments)) +} + /// Convenience wrapper: prove then verify in one call (the original integrated API). /// Returns `Ok(Some(public_output))` iff the continuation proves and verifies. pub fn prove_and_verify_continuation( @@ -1504,9 +1581,9 @@ mod tests { let page_size = page::DEFAULT_PAGE_SIZE; let max = page::max_private_input_pages(); - // (64 MiB + 4-byte prefix) / 256 KiB page = 257 pages (256 full data pages plus + // (512 MiB + 4-byte prefix) / 256 KiB page = 2049 pages (2048 full data pages plus // the one page the length prefix spills into). Pinned so a size/page change is caught. - assert_eq!(max, 257); + assert_eq!(max, 2049); // No slack: an honest MAX-size input needs the whole last page (the bound is not // padded), and never overflows into an extra one. diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 6bb3b1247..54cdbe505 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,6 +292,81 @@ pub fn verify_and_attest_blob( Ok(Some(attestation)) } +/// [`verify_and_attest_blob`]'s core for a continuation bundle (the +/// `continuation` guest feature): verify every epoch + the global memory +/// proof against the supplied roots, then attest +/// `program_id(elf, roots) || public_output`. The fold uses +/// the same [`program_id`] as the monolithic path but over the continuation's +/// root set (DECODE + touched data-page genesis roots), so a consumer re-binds +/// with [`crate::continuation::continuation_precomputed_commitments`] over the +/// bundle it holds — the touched-page set is bundle-dependent, unlike the +/// monolithic path's ELF-only page set. +pub fn verify_continuation_and_attest( + bundle: &crate::continuation::ContinuationProof, + elf_bytes: &[u8], + proof_options: &ProofOptions, + decode_commitment: Commitment, + page_commitments: &[(u64, Commitment)], +) -> Result>, Error> { + let Some(public_output) = crate::continuation::verify_continuation_with_roots( + elf_bytes, + bundle, + proof_options, + Some(decode_commitment), + Some(page_commitments), + )? + else { + return Ok(None); + }; + let id = program_id_from_elf(elf_bytes, &decode_commitment, page_commitments)?; + let mut attestation = id.to_vec(); + attestation.extend_from_slice(&public_output); + Ok(Some(attestation)) +} + +/// [`verify_continuation_and_attest`] over the wire-format blob +/// ([`encode_continuation_guest_input`]) — the `continuation` guest's whole +/// job in one call. The archive is bytecheck-validated, then the input is +/// materialized with one `rkyv::deserialize` pass (a structured copy, not a +/// parse) because the epoch/global verifiers currently consume an owned +/// [`crate::continuation::ContinuationProof`]; verifying epochs zero-copy over +/// the archived bundle (as [`crate::verify_recursion_blob`] does for the +/// monolithic proof) is follow-up work. +pub fn verify_continuation_and_attest_blob( + blob: &[u8], + proof_options: &ProofOptions, +) -> Result>, Error> { + use rkyv::rancor::Error as RkyvError; + + let archive_bytes = crate::recursion_archive_bytes(blob).ok_or_else(|| { + Error::Execution(String::from( + "continuation recursion blob: bad magic or version", + )) + })?; + // Host callers' Vec carries no alignment guarantee; the guest slice is + // aligned by construction (same prefix arithmetic as the monolithic blob). + let mut aligned_fallback = rkyv::util::AlignedVec::<{ crate::RECURSION_INPUT_ALIGN }>::new(); + let archive: &[u8] = + if (archive_bytes.as_ptr() as usize).is_multiple_of(crate::RECURSION_INPUT_ALIGN) { + archive_bytes + } else { + aligned_fallback.extend_from_slice(archive_bytes); + &aligned_fallback + }; + let archived = rkyv::access::(archive) + .map_err(|e| Error::Execution(format!("continuation blob validation failed: {e}")))?; + let input: ContinuationGuestInput = rkyv::deserialize::<_, RkyvError>(archived) + .map_err(|e| Error::Execution(format!("continuation blob deserialize failed: {e}")))?; + + verify_continuation_and_attest( + &input.bundle, + &input.inner_elf, + proof_options, + input.decode_commitment, + &input.page_commitments, + ) +} + /// Split committed attestation bytes into `(program_id, inner_public_output)`. /// `None` if too short to contain an id. pub fn split_attestation(committed: &[u8]) -> Option<([u8; 32], &[u8])> { diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index 15817df3f..de675e4c1 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -531,6 +531,69 @@ fn test_recursion_blob_decodes_and_verifies_on_host() { assert!(v.ok, "misaligned-buffer verify must also succeed"); } +/// Continuation flavor of the roundtrip guard: prove the empty program via +/// continuations (tiny epochs so the bundle is genuinely multi-epoch), encode +/// the [`recursion::ContinuationGuestInput`] blob, decode it exactly as the +/// `continuation`-feature guest does, and mirror its +/// `verify_continuation_and_attest` call — a cheap host-side check of the +/// encode/decode/verify/attest contract without running the VM. +#[test] +fn test_recursion_continuation_blob_decodes_and_verifies_on_host() { + let root = workspace_root(); + let fib_elf_bytes = read_guest_elf(&root, "fibonacci"); + let inner_input = 10u64.to_le_bytes(); + + let bundle = crate::continuation::prove_continuation( + &fib_elf_bytes, + &inner_input, + 4, + &MIN_PROOF_OPTIONS, + ) + .expect("continuation prove should succeed"); + assert!( + bundle.num_epochs() > 1, + "epoch=2^4 must split fibonacci(10) into multiple epochs for this test to bite" + ); + // Ground truth: the trustless recompute path must accept the bundle. + let expected_output = + crate::continuation::verify_continuation(&fib_elf_bytes, &bundle, &MIN_PROOF_OPTIONS) + .expect("verify_continuation errored") + .expect("bundle must verify with recomputed roots"); + // Consumer re-bind values, computed before the encode consumes the bundle: + // recompute the roots from the bundle + trusted ELF and compare ids (the + // continuation analog of check_attestation). + let (expected_decode, expected_pages) = + crate::continuation::continuation_precomputed_commitments( + &fib_elf_bytes, + &bundle, + &MIN_PROOF_OPTIONS, + ) + .expect("continuation_precomputed_commitments errored"); + let expected_id = + recursion::program_id_from_elf(&fib_elf_bytes, &expected_decode, &expected_pages) + .expect("program_id_from_elf errored"); + + let blob = + recursion::encode_continuation_guest_input(bundle, &fib_elf_bytes, &MIN_PROOF_OPTIONS) + .expect("encode_continuation_guest_input failed"); + + // Verify exactly as the guest does (built with `continuation` + `min`): + // prefix validation + rkyv access + deserialize + verify + attest. + let attestation = recursion::verify_continuation_and_attest_blob(&blob, &MIN_PROOF_OPTIONS) + .expect("verify_continuation_and_attest_blob errored") + .expect("continuation proof did not survive the rkyv round-trip"); + let (id, output) = recursion::split_attestation(&attestation).expect("attestation too short"); + assert_eq!( + id, expected_id, + "attested id must match the honest recompute" + ); + assert_eq!( + output, + &expected_output[..], + "supplied-roots output must match the recompute path's output" + ); +} + /// Corrupting a private-input commitment on an *honest* proof makes /// verification fail (`Ok(false)`). Necessary but not sufficient alone — a /// custom prover can supply consistent mismatched roots (see @@ -708,19 +771,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. @@ -737,6 +874,22 @@ 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); +} + /// 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 59053bb2668751db0d9cc8a6d3d1d0cc31c1ffff Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 16 Jul 2026 12:38:11 -0300 Subject: [PATCH 2/2] fix(syscalls): raise the guest private-input clamp to 512 MiB The 64 MiB -> 512 MiB bump this branch claims only ever landed on the executor half. `executor::vm::memory::MAX_PRIVATE_INPUT_SIZE` is 512 MiB, but the guest-side twin here stayed at 64 MiB, so `get_private_input_slice` clamped `len.min(64 MiB)` and handed the guest the first 64 MiB of any larger blob. A real recursion input (4tx/blowup2 = 143,004,640 bytes) arrived truncated, rkyv rejected the fragment, and the recursion guest surfaced it as an opaque Panic("PANICKED") ~1s in. That made the ladder in `scripts/bench_recursion_scaling.sh` unrunnable on this branch: its own header example (blob=145080513 with a real cycle count) is only reachable with the guest clamp at 512 MiB, so the value was 512 MiB when the table was measured and the edit was lost before commit. The published numbers are unaffected -- re-measuring 4tx/blowup2 at a8a3d870 reproduces 13.36B cycles against the recorded 13.24B at c30ffe5a (+0.94%, i.e. #815's own cost). Soundness is unchanged: the clamp exists to bound a forged length prefix, and it still bounds it -- now at the same cap the host enforces in `Memory::store_private_inputs`, which is what makes an honest prefix always `<=` the bound. --- syscalls/src/syscalls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index ad9947855..7165dff81 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -8,14 +8,14 @@ use core::arch::asm; #[cfg(target_arch = "riscv64")] pub const PRIVATE_INPUT_START: usize = 0xFF000000; -/// Maximum private-input length the guest will read, in bytes (64 MiB). +/// Maximum private-input length the guest will read, in bytes (512 MiB). /// The host caps stored input at this size in `Memory::store_private_inputs`, /// so an honest length prefix is always `<=` this bound; a larger value can only /// come from a malformed or forged prefix. The reader clamps to this cap so a /// bogus length can never make the guest fabricate an arbitrarily long slice. /// Must match `executor::vm::memory::MAX_PRIVATE_INPUT_SIZE`. #[cfg(target_arch = "riscv64")] -const MAX_PRIVATE_INPUT_SIZE: usize = 64 * 1024 * 1024; +const MAX_PRIVATE_INPUT_SIZE: usize = 512 * 1024 * 1024; #[cfg(target_arch = "riscv64")] pub enum SyscallNumbers {