Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions .github/workflows/bench-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ jobs:
startsWith(github.event.comment.body, '/bench-verify') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: [self-hosted, bench]
# Job cap. The verifier bench is ~5-6 min and the recursion measurement itself ~1 min,
# but on a cold runner the recursion BUILDS dominate: MEASURE_CLI (release cli) once,
# plus PER REF a guest build (~10-20 min) and a prover-test build for the blob dump.
# All cached in /tmp for later runs, and build-std / the host cargo target are shared
# Job cap. On a cold runner the recursion BUILDS dominate: MEASURE_CLI (release cli)
# once, plus PER REF a guest build and a prover-test build for the blob dump. All
# cached in /tmp for later runs, and build-std / the host cargo target are shared
# across ref worktrees (see the recursion step's env) to keep the cold run under this
# cap. The recursion step also has its own tighter timeout so a runaway build there
# can't burn the whole job and lose the already-captured verifier result.
Expand All @@ -46,7 +45,7 @@ jobs:
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number,
body: '⏳ **Benchmark started** on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.'
body: '⏳ **Benchmark started** on the bench server. The recursion-guest cycle comparison adds guest builds on top of the verifier bench, longer on a cold runner. The bench server is occupied until it finishes.'
});

- name: Resolve PR head + pair count
Expand Down Expand Up @@ -92,10 +91,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
# per ref a guest build (~10-20 min) and a prover-test build for the blob dump; the
# Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main),
# in three regimes: `min` (blowup=2, 1 query — cheap diagnostic floor) plus the
# realistic full-query base-layer points `blowup2` (219 queries) and `blowup4`
# (110 queries). Each measurement is one exact `execute --cycles` reading per ref
# (no ABBA). Cold wall time is dominated by BUILDS: MEASURE_CLI (release cli)
# once, plus a guest build and a prover-test build for the blob dump per ref; the
# GUEST_TARGET_DIR / HOST_TARGET_DIR below share build-std and the host cargo target
# across the two ref worktrees so the second ref is much cheaper (and per-worktree
# disk shrinks). continue-on-error + `!cancelled()` keep this fully isolated from the
Expand All @@ -105,11 +106,9 @@ jobs:
id: recursion
if: ${{ !cancelled() }}
continue-on-error: true
# Fail-fast well under the 90-min job cap so a runaway recursion build can't burn
# Fail-fast well under the job cap so a runaway recursion build can't burn
# the whole job and lose the already-captured verifier result (continue-on-error
# absorbs the timeout; the job still posts the verifier verdict). Sized with
# headroom over a cold shared-build run (MEASURE_CLI + 2 guest builds + 2 blob-dump
# builds).
# absorbs the timeout; the job still posts the verifier verdict).
timeout-minutes: 70
env:
HEAD_SHA: ${{ steps.cfg.outputs.head_sha }}
Expand All @@ -122,6 +121,26 @@ jobs:
set -o pipefail
scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main min 2>&1 | tee /tmp/recursion_out.txt
sed -n '/=== Recursion-guest cycle/,$p' /tmp/recursion_out.txt > /tmp/recursion_result.txt
# Full-query regimes: the realistic base-layer proofs (the single-query `min`
# regime above is a diagnostic floor, not the real verifier cost). Guest builds
# and worktrees are already cached from the min run, so each adds only a blob
# dump and one execute per ref. Both refs' dump tests must understand
# RECURSION_DUMP_PRESET to produce a comparable blob for a given preset — until
# `origin/main` merges the preset-aware dump test, it can only ever dump `min`
# options, so every blowup2/blowup4 attempt against it would fail the same way.
# Check that support once, up front, instead of attempting (and failing) each
# preset in turn.
if git grep -q RECURSION_DUMP_PRESET origin/main -- prover/src/tests/ 2>/dev/null; then
for preset in blowup2 blowup4; do
if scripts/bench_recursion_cycles.sh "$HEAD_SHA" origin/main "$preset" 2>&1 | tee "/tmp/recursion_out_${preset}.txt"; then
{ echo; sed -n '/=== Recursion-guest cycle/,$p' "/tmp/recursion_out_${preset}.txt"; } >> /tmp/recursion_result.txt
else
{ echo; echo "_(${preset} full-query regime unavailable for these refs — see the workflow log.)_"; } >> /tmp/recursion_result.txt
fi
done
else
{ echo; echo "_(blowup2/blowup4 full-query regimes need \`origin/main\` to have the preset-aware dump test (RECURSION_DUMP_PRESET) — not merged yet, so only \`min\` is compared for this PR.)_"; } >> /tmp/recursion_result.txt
fi

- name: Post result
if: always()
Expand Down
40 changes: 32 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \
clean-recursion-elfs clean test test-asm \
test-rust test-ethrex test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \
test-profile-recursion-block \
test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \
test-prover-cuda test-prover-comprehensive-cuda \
bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \
Expand Down Expand Up @@ -56,13 +57,23 @@ 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-<preset>-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-<preset>-bench) to its own artifact, via the
# define/foreach/eval below rather than the generic %.elf pattern rule.
# `required-features` on each [[bin]] is a subset match, not an exact-set
# match, so e.g. `--features "continuation min"` (the cont-min build) also
# satisfies plain `recursion-min-bench`'s `required-features = ["min"]` and
# cargo builds it too — racing with a concurrent `make -j` job building
# `recursion-min.elf` (`--features min`) to that same shared-target-dir path.
# `--bin $(2)` in build_guest_elf pins each invocation to its one target bin.
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.
Expand Down Expand Up @@ -191,6 +202,7 @@ cd $(1) && \
-Z build-std=core,alloc,std,compiler_builtins,panic_abort \
-Z build-std-features=compiler-builtins-mem \
-Z json-target-spec \
--bin $(2) \
$(3)
cp $(SHARED_TARGET_DIR)/riscv64im-lambda-vm-elf/release/$(2) $@
endef
Expand All @@ -215,7 +227,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-<preset>-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
Expand All @@ -236,6 +248,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-<preset>-bench -> recursion-cont-<preset>.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)

Expand Down Expand Up @@ -278,6 +298,10 @@ test-profile-recursion-single: compile-recursion-elfs
test-profile-recursion-multi: compile-recursion-elfs
cargo test --package lambda-vm-prover --lib test_recursion_profile_multiquery -- --ignored --nocapture

# Real-block profile (ethrex, blowup=4/4 transfers), via the `continuation` guest.
test-profile-recursion-block: compile-recursion-elfs $(RUST_ARTIFACTS_DIR)/ethrex.elf
cargo test --package lambda-vm-prover --lib --release test_recursion_profile_blowup4_block -- --ignored --nocapture

# Regenerate the committed ethrex block fixtures (see tooling/ethrex-fixtures).
# Run after bumping the ethrex rev; README checksums are refreshed automatically.
regen-ethrex-fixtures:
Expand Down
37 changes: 34 additions & 3 deletions bench_vs/lambda/recursion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<preset>-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",
Expand Down
56 changes: 44 additions & 12 deletions bench_vs/lambda/recursion/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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` — 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.
//!
Expand All @@ -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;

Expand All @@ -65,9 +89,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();
}
6 changes: 6 additions & 0 deletions executor/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
/program_artifacts/rust
/tests/ethrex_hoodi.bin
/tests/ethrex_bench_*.bin
# _4 is committed: prover/src/tests/recursion_smoke_test.rs reads it directly
# (~17 KB), and scripts/bench_recursion_scaling.sh also reads it as part of its
# scaling ladder. The other ladder sizes (_1/_8/_16) and bigger ones (e.g. _20)
# stay ignored — bench_recursion_scaling.sh generates any missing fixture on
# demand via tooling/ethrex-fixtures.
!/tests/ethrex_bench_4.bin
Binary file added executor/tests/ethrex_bench_4.bin
Binary file not shown.
Loading
Loading