Skip to content
Merged
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
63 changes: 38 additions & 25 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ RECURSION_ARTIFACTS := $(addprefix $(RECURSION_ARTIFACTS_DIR)/, $(addsuffix .elf

# 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) — so it's built as two named artifacts
# from the same crate dir, not via the generic %.elf pattern rule.
# 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)))

Expand Down Expand Up @@ -149,10 +151,10 @@ compile-programs-rust: prepare-sysroot $(RUST_ARTIFACTS)

compile-bench: prepare-sysroot $(BENCH_ARTIFACTS)

# NOTE: the recursion smoke tests are #[ignore]d (not run by `make test` /
# `test-executor`) because they're too slow for CI today; only `test-prover-all`
# runs them. We still compile their guest ELFs on every build so they keep
# compiling until the tests are fast enough to run in CI.
# NOTE: the recursion smoke tests read these prebuilt guest ELFs. The fast ones
# run on every `cargo test` (so `make test`, which depends on this target, needs
# them); the slow ones stay #[ignore]d (only `test-prover-all` runs them). We
# compile the guest ELFs on every build so the tests always have them ready.
compile-programs: compile-programs-asm compile-programs-rust compile-bench compile-recursion-elfs

compile-recursion-elfs: prepare-sysroot $(RECURSION_ARTIFACTS) $(RECURSION_VERIFIER_ARTIFACTS)
Expand Down Expand Up @@ -213,20 +215,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)

# Both presets build the same crate to the same CARGO_TARGET_DIR / same
# release/recursion-bench, so the post-lock cp races under `make -j` (one
# preset's cp reads the file while the other overwrites it). Serialize them.
.NOTPARALLEL: $(RECURSION_VERIFIER_ARTIFACTS)

# The recursion verifier's `min`/`blowup8` presets: same crate dir, same
# built-binary filename, different Cargo feature -> different artifact name.
# Not a pattern rule (the stem "recursion-min" wouldn't match the crate dir
# "recursion") — see the comment on RECURSION_VERIFIER_PRESETS above.
$(RECURSION_ARTIFACTS_DIR)/recursion-min.elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR)
$(call build_guest_elf,$(RECURSION_GUESTS_DIR)/recursion,recursion-bench,--features min)

$(RECURSION_ARTIFACTS_DIR)/recursion-blowup8.elf: FORCE | prepare-sysroot $(RECURSION_ARTIFACTS_DIR)
$(call build_guest_elf,$(RECURSION_GUESTS_DIR)/recursion,recursion-bench,--features blowup8)
# The recursion verifier's `min`/`blowup8` 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
# 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).
# $(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).
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))))

clean-asm:
-rm -rf $(ASM_ARTIFACTS_DIR)
Expand Down Expand Up @@ -294,13 +302,16 @@ test-fast: compile-recursion-elfs
test-prover: compile-recursion-elfs
cargo test -p lambda-vm-prover

# Prover tests including slow ones. The recursion smoke tests (#[ignore]d) read
# prebuilt guest ELFs from executor/program_artifacts/recursion/, so build them first.
# Prover tests including slow ones. The recursion smoke tests read prebuilt
# guest ELFs from executor/program_artifacts/recursion/ — the fast ones on every
# run, the slow ones (still #[ignore]d) only under --include-ignored — so build
# them first.
test-prover-all: compile-recursion-elfs
cargo test -p lambda-vm-prover -- --include-ignored

# Prover tests with debug-checks (shows bus balance report)
test-prover-debug:
# Prover tests with debug-checks (shows bus balance report). Also unfiltered, so
# it runs the non-ignored recursion tests that read prebuilt guest ELFs.
test-prover-debug: compile-recursion-elfs
cargo test -p lambda-vm-prover --features debug-checks -- --nocapture

# Disk-spill tests (stark + prover). FORCE_DISK_SPILL is required by the prover tests.
Expand Down Expand Up @@ -330,7 +341,9 @@ test-cuda-fallback:
# GPU + nvcc). The GPU CI counterpart of CPU CI's sharded prover tests. Single-threaded: the
# GPU serializes proves and the dispatch counters are process-global. cuda on prover cascades
# to stark; crypto/ecsm build without it (they have no GPU path).
test-prover-cuda:
# compile-recursion-elfs: this unfiltered run executes the non-ignored recursion
# smoke tests, which read prebuilt guest ELFs; scripts/gpu_test.sh otherwise never builds them.
test-prover-cuda: compile-recursion-elfs
cargo test --release -p lambda-vm-prover -p stark -p crypto -p ecsm \
--features lambda-vm-prover/cuda -- --test-threads=1

Expand Down
1 change: 1 addition & 0 deletions bench_vs/lambda/recursion/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions bench_vs/lambda/recursion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,27 @@ edition = "2024"
[features]
# 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
# 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.
min = []
blowup8 = []

# 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.
[[bin]]
name = "recursion-min-bench"
path = "src/main.rs"
required-features = ["min"]

[[bin]]
name = "recursion-blowup8-bench"
path = "src/main.rs"
required-features = ["blowup8"]

[dependencies]
lambda-vm-prover = { path = "../../../prover", default-features = false, features = [
"profile-markers",
Expand Down
83 changes: 27 additions & 56 deletions bench_vs/lambda/recursion/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,37 @@
//! Naive recursion guest: verifies an inner lambda-vm proof inside the VM.
//!
//! Private input (postcard): `(VmProof, Vec<u8>, Commitment, Vec<(u64, Commitment)>)`
//! — the inner program's ELF bytes plus its precomputed DECODE and
//! Private input (postcard): `lambda_vm_prover::recursion::GuestInput` — the
//! inner proof, the inner program's ELF bytes, and its precomputed DECODE and
//! ELF-data-page commitments, supplied instead of recomputed in-VM.
//! `verify_with_options` does NOT bind the supplied roots to `inner_elf`; that
//! binding is established by folding them into `program_id` (below) and having
//! the host recompute that id and compare. That recompute is expensive, so it
//! happens once at the top level in the host, never in the guest — see
//! `program_id` in the prover's `statement` module.
//!
//! `ProofOptions` is fixed by the `min`/`blowup8` Cargo feature, not private
//! input (an attacker could otherwise pick trivially weak options and have the
//! guest accept as if a real proof had been checked).
//! `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.
//!
//! On success commits `program_id(inner_elf, decode_commitment,
//! page_commitments) || inner_public_output` — the program identity (a fold
//! pinning the ELF together with the roots it was verified against) plus the
//! result the inner proof attested.
//! On success commits `program_id || inner_public_output` via
//! `recursion::verify_and_attest` (a single ELF parse and a single full-ELF
//! Keccak, shared between the statement absorb and the `program_id` fold). The
//! attestation is not self-enforcing: the binding is established by the
//! consumer via `recursion::check_attestation` (a host-side recompute+compare),
//! never in-guest.
//!
//! std (not `no_std`): `build-std` provides it, prove-side code is DCE'd.
//! `#![no_main]`; inits the syscalls global allocator first thing in `main`.

#![no_main]

#[cfg(feature = "blowup8")]
use lambda_vm_prover::GoldilocksCubicProofOptions;
use lambda_vm_prover::{Commitment, ProofOptions, VmProof};
use lambda_vm_prover::recursion::{GuestInput, 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");

/// Smallest possible proof options (blowup=2, 1 query). Intentionally
/// insecure — for cheap diagnostics, not soundness.
/// The build preset fixing the inner `ProofOptions` (see the module docs).
#[cfg(feature = "min")]
fn recursion_proof_options() -> ProofOptions {
ProofOptions {
blowup_factor: 2,
fri_number_of_queries: 1,
coset_offset: 3,
grinding_factor: 1,
fri_final_poly_log_degree: 7,
}
}

/// 128-bit security (multi-query).
const PRESET: Preset = Preset::Min;
#[cfg(feature = "blowup8")]
fn recursion_proof_options() -> ProofOptions {
GoldilocksCubicProofOptions::with_blowup(8).expect("blowup=8 is always valid")
}
const PRESET: Preset = Preset::Blowup8;

#[unsafe(export_name = "main")]
pub fn main() -> ! {
Expand All @@ -62,37 +44,26 @@ pub fn main() -> ! {
}));

let blob = lambda_vm_syscalls::syscalls::get_private_input();
let (vm_proof, inner_elf, decode_commitment, page_commitments): (
VmProof,
Vec<u8>,
Commitment,
Vec<(u64, Commitment)>,
) = postcard::from_bytes(&blob).expect("failed to deserialize recursion input");
let (vm_proof, inner_elf, decode_commitment, page_commitments): GuestInput =
postcard::from_bytes(&blob).expect("failed to deserialize recursion input");
lambda_vm_prover::profile_markers::step_marker::<
{ lambda_vm_prover::profile_markers::STEP_DECODE_DONE },
>();

let options = recursion_proof_options();
let ok = lambda_vm_prover::verify_with_options(
// The guest's whole job: verify the inner proof against the supplied roots
// and, on success, produce `program_id || inner_public_output`. The id fold
// is what the consumer rebinds to a trusted ELF (`check_attestation`); it is
// not self-enforcing here.
let options = PRESET.options();
let attestation = lambda_vm_prover::recursion::verify_and_attest(
&vm_proof,
&inner_elf,
&options,
Some(decode_commitment),
Some(&page_commitments),
)
.expect("verify errored");
assert!(ok, "inner proof failed verification");

// program_id is not self-enforcing: a consumer must recompute it natively
// and reject on mismatch. Commit the inner output alongside it.
let id = lambda_vm_prover::statement::program_id_from_elf(
&inner_elf,
&decode_commitment,
decode_commitment,
&page_commitments,
)
.expect("program_id");
let mut output = id.to_vec();
output.extend_from_slice(&vm_proof.public_output);
lambda_vm_syscalls::syscalls::commit(&output);
.expect("verify errored")
.expect("inner proof failed verification");
lambda_vm_syscalls::syscalls::commit(&attestation);
lambda_vm_syscalls::syscalls::sys_halt();
}
4 changes: 3 additions & 1 deletion prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ math = { path = "../crypto/math" }
executor = { path = "../executor" }
ecsm = { path = "../crypto/ecsm" }
serde = { version = "1.0", features = ["derive"] }
# The recursion guest-input blob codec (see `recursion::encode_guest_input`);
# no_std+alloc, so the in-VM guest build (default-features = false) is fine.
postcard = { version = "1.0", features = ["alloc"] }
rayon = { version = "1.8.0", optional = true }
sysinfo = { version = "0.31", default-features = false, features = ["system"] }
log = "0.4"
Expand All @@ -30,7 +33,6 @@ sha3 = { version = "0.10.8", default-features = false }
env_logger = "*"
criterion = { version = "0.5", default-features = false }
bincode = "1"
postcard = { version = "1.0", features = ["alloc"] }
tikv-jemallocator = "0.6"
tikv-jemalloc-ctl = { version = "0.6", features = ["stats"] }
tiny-keccak = { version = "2.0", features = ["keccak"] }
Expand Down
Loading
Loading