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 += ( diff --git a/.github/scripts/run_recursion_bench.sh b/.github/scripts/run_recursion_bench.sh new file mode 100755 index 000000000..05e7f5b1d --- /dev/null +++ b/.github/scripts/run_recursion_bench.sh @@ -0,0 +1,45 @@ +#!/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" + 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 + { 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 baf550bac..0641195cc 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,20 +30,19 @@ 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. 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 - # 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) + if: github.event_name == 'issue_comment' uses: actions/github-script@v7 with: script: | @@ -46,7 +53,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 @@ -55,14 +62,25 @@ 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 + 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 @@ -74,6 +92,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 @@ -92,24 +111,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). - # 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 - # 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 - # 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 90-min 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). + # 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 }} @@ -119,9 +134,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 + .github/scripts/run_recursion_bench.sh "$HEAD_SHA" - name: Post result if: always() @@ -163,6 +176,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, @@ -180,3 +198,10 @@ jobs: issue_number: context.issue.number, body }); } + + # 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: | + echo "::error::Recursion cycle bench step did not complete (outcome=${{ steps.recursion.outcome }}) — see its log and the posted result above." + exit 1 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 12ac291f5..4abed4a90 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 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 \ @@ -56,13 +57,21 @@ 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 -# 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))) +# 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. +# `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` 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))) # Override with: make ... SYSROOT_DIR=$HOME/.lambda-vm-sysroot # to install the sysroot in a user-writable location and avoid sudo. @@ -191,6 +200,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 @@ -215,27 +225,26 @@ $(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 -# 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)) 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 +287,27 @@ 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 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 + +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 $(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). # 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..1a846109b 100644 --- a/bench_vs/lambda/recursion/src/main.rs +++ b/bench_vs/lambda/recursion/src/main.rs @@ -12,15 +12,21 @@ //! `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 (`recursion::ContinuationGuestInput`, built by +//! `recursion::encode_continuation_guest_input`), verified via +//! `recursion::verify_continuation_and_attest` — same trust model, 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 +37,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 +87,17 @@ 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, &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..17f7497d7 100644 --- a/executor/.gitignore +++ b/executor/.gitignore @@ -2,3 +2,7 @@ /program_artifacts/rust /tests/ethrex_hoodi.bin /tests/ethrex_bench_*.bin +# _4 is committed (~17 KB): used by `make recursion-profile-block-input` and +# scripts/bench_recursion_scaling.sh. Other sizes stay ignored — scaling.sh +# generates any missing fixture on demand via tooling/ethrex-fixtures. +!/tests/ethrex_bench_4.bin diff --git a/executor/tests/ethrex_bench_4.bin b/executor/tests/ethrex_bench_4.bin new file mode 100644 index 000000000..45fe93038 Binary files /dev/null and b/executor/tests/ethrex_bench_4.bin differ diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 654b58fa4..efca722c9 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-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-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; @@ -52,15 +53,33 @@ 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, 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. + Blowup4, + /// Blowup=8, 73 queries — 128-bit, final-wrap-style parameters. 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 +90,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 +100,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 +150,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 +289,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..1f4800011 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -155,9 +155,17 @@ 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 before the guest runs and +/// checked against its committed attestation in [`run_profile_from`]. +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 +173,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 +197,53 @@ 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) +} + +/// [`setup_guest_run`]'s fixture-based counterpart for a real ethrex block: +/// 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, + executor::vm::execution::Executor, + ExpectedAttestation, +) { + let root = workspace_root(); + let guest_elf_bytes = read_guest_elf(&root, "recursion-cont-blowup4"); + + 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: id_bytes + .try_into() + .expect("expected sidecar id is 32 bytes"), + output: output.to_vec(), + }; + + let program = executor::elf::Elf::load(&guest_elf_bytes).expect("ELF load failed"); + assert_ne!( + 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"); + (guest_elf_bytes, program, executor, expected) } /// Demangled enclosing-function name for a PC via the ELF symbol table; @@ -324,16 +385,40 @@ 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, expected) = setup_guest_run("profile", preset); + run_profile_from( + preset, + &guest_elf_bytes, + &program, + executor, + progress_stride, + detailed, + &expected, + ); +} + +/// 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], + program: &executor::elf::Elf, + mut executor: executor::vm::execution::Executor, + progress_stride: usize, + detailed: bool, + expected: &ExpectedAttestation, +) { 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"); @@ -386,6 +471,29 @@ fn run_profile(preset: Preset, progress_stride: usize, detailed: bool) { }, ); + // 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") + .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!( @@ -674,7 +782,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"); @@ -771,19 +879,129 @@ 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`) — 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() { 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(), + }; + + // Continuation dumps also get an `.expected` sidecar (32-byte id || inner + // 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). + 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}")); + 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()); + + 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( + "dump-input", + &inner_elf_bytes, + &inner_input, + &preset.options(), + ); + (blob, None) + } + }; + 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() + ); + + 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. @@ -800,6 +1018,41 @@ 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 recursion `continuation` guest +/// 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() { + let (guest_elf_bytes, program, executor, expected) = setup_block4_blowup4_guest_run(); + run_profile_from( + Preset::Blowup4, + &guest_elf_bytes, + &program, + executor, + 500, + true, + &expected, + ); +} + /// 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..c4bc06461 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,21 @@ # 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); 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. @@ -55,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 @@ -62,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 @@ -104,8 +116,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 } @@ -168,11 +180,36 @@ valid_result() { measure_ref() { local ref="$1" sha="$2" role="$3" local sha8="${sha:0:8}" - # 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" + # `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}" + + # 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 + 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 @@ -196,16 +233,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). - echo "==> [$role] make compile-recursion-elfs @ $sha8 (this can take 10-20 min the first time) ..." >&2 + # 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.) @@ -214,10 +256,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" @@ -229,42 +278,68 @@ measure_ref() { fi echo "==> [$role] guest ELF: $(basename "$guest_elf")" >&2 - # 2c. Generate this ref's own input blob via its ignored dump test. - 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 - rm -f /tmp/recursion_input.bin - local dlog="$WORK/dump_${sha8}.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 - 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" && cargo test -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). 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 +409,14 @@ 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)" ;; + 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 new file mode 100755 index 000000000..c868586db --- /dev/null +++ b/scripts/bench_recursion_scaling.sh @@ -0,0 +1,119 @@ +#!/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. +# +# 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: +# TXS="1 4 8 16" block sizes (transfers); fixtures are read from +# 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). +# 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)" +trap 'rm -rf "$WORK"' EXIT + +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; 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 + 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}')" + 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 ' ')" + + 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"