Skip to content

feat: enable recursion#742

Merged
MauroToscano merged 6 commits into
mainfrom
pr/recursion-guest
Jun 30, 2026
Merged

feat: enable recursion#742
MauroToscano merged 6 commits into
mainfrom
pr/recursion-guest

Conversation

@Oppen

@Oppen Oppen commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add recursion guest programs in bench_vs/lambda/: empty (no_std halt, inner program under test) and recursion (std verifier guest that deserializes a postcard-encoded (VmProof, elf, ProofOptions) and calls verify_with_options).
  • Makefile: compile-recursion-elfs / clean-recursion-elfs targets; test-prover-all depends on compile-recursion-elfs.
  • Raise MAX_PRIVATE_INPUT_SIZE from 6.7 MB → 64 MiB to fit a real VmProof as private input.
  • Re-export ProofOptions from the prover crate so the verifier guest can name it without a direct stark dep.
  • Add prover/src/tests/recursion_smoke_test.rs with ignored tests at two tiers: execute-only (in-VM verify, skips outer STARK prove) and full-prove (outer proof + host verify).
  • Implement deterministic randomness for non-cryptographic use, needed for HashMap access in the guest

How to test

Build the guest ELFs first:

make compile-recursion-elfs

Run the cheapest regression guard (host-only encode/decode + verify, seconds):

cargo test -p lambda-vm-prover -- --include-ignored test_recursion_blob_decodes_and_verifies_on_host

Execute-only tier (in-VM verify, no outer prove, tens of GB RAM):

cargo test -p lambda-vm-prover -- --include-ignored test_recursion_execute_1query
cargo test -p lambda-vm-prover -- --include-ignored test_recursion_execute_empty

Full pipeline (outer STARK prove + verify, ~125 GB RAM):

cargo test -p lambda-vm-prover -- --include-ignored test_recursion_prove_1query

Or via make (builds ELFs + runs all ignored prover tests):

make test-prover-all

@Oppen

Oppen commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

/ai-review
/bench

@github-actions

Copy link
Copy Markdown

Codex Code Review

Found 2 issues:

  1. Medium - public APIs expose crate-private hash aliases
    crypto/stark/src/lib.rs, crypto/stark/src/lookup.rs, prover/src/lib.rs, prover/src/tables/page.rs

    det_hash is pub(crate), but public structs/type aliases now expose HashMap from that private module, e.g. BusPublicInputs debug fields, FinalStateMap, FinalRegisterStateMap, FiniStateMap, and epoch_boundaries params. This is likely to trigger private_interfaces under -D warnings and also makes public API types harder for downstream crates to name. Make the deterministic hash aliases public, or avoid using the private alias in public signatures.

  2. Low - normal make test now builds ignored recursion guests
    Makefile

    compile-programs now depends on compile-recursion-elfs, so make test and make test-executor build the full recursion guest even though all recursion smoke tests are #[ignore] and test-prover-all already depends on compile-recursion-elfs. That adds a heavy guest build to routine test paths with no test coverage benefit. Keep recursion ELF builds scoped to test-prover-all and the explicit compile-recursion-elfs target.

@Oppen Oppen marked this pull request as draft June 29, 2026 21:46
@Oppen Oppen force-pushed the pr/recursion-guest branch from b628a1d to 619e262 Compare June 29, 2026 22:54
@yetanotherco yetanotherco deleted a comment from claude Bot Jun 29, 2026
@yetanotherco yetanotherco deleted a comment from claude Bot Jun 29, 2026
@yetanotherco yetanotherco deleted a comment from github-actions Bot Jun 29, 2026
@yetanotherco yetanotherco deleted a comment from github-actions Bot Jun 29, 2026
@yetanotherco yetanotherco deleted a comment from claude Bot Jun 29, 2026
@yetanotherco yetanotherco deleted a comment from claude Bot Jun 29, 2026
@Oppen

Oppen commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

/bench
/ai-review

@Oppen Oppen force-pushed the pr/recursion-guest branch from 619e262 to fcf20ef Compare June 29, 2026 22:56
@github-actions

Copy link
Copy Markdown

Codex Code Review

Findings

  • Medium - recursion guest terminates without HALT ecall: bench_vs/lambda/recursion/src/main.rs exports main directly and returns i32, but this guest’s config sets the ELF entrypoint to main, so it bypasses _start, which is the wrapper that calls sys_halt(). The executor may stop after returning to pc = 0, but proof trace construction requires a final ecall and returns MissingHaltEcall when building final traces. This breaks both prove_outer_and_commit and the execute-only path’s trace build. Make this guest halt explicitly after commit(&[1u8]), or enter through _start instead of main.

Notes

I reviewed only the PR diff and nearby dependencies. I did not build or run tests per the static-review constraints.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 3.4 ± 0.1 3.3 3.6 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 138.7 ± 2.5 135.3 142.7 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 134.0 ± 1.6 131.7 136.6 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 90.7 ± 0.4 90.3 91.6 1.00

Comment thread bench_vs/lambda/recursion/Cargo.toml Outdated
Comment thread executor/src/vm/memory.rs
@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review: feat: enable recursion

Reviewed the non-generated changes (skipping Cargo.lock). This is mostly tooling + ignored smoke tests; the runtime-affecting changes are small and look sound. No critical or high-severity issues found.

Verified as correct (called out because they looked risky at first glance)

  • recursion guest returning i32 instead of halting (bench_vs/lambda/recursion/src/main.rs): unlike empty/fibonacci which end in an explicit halt() ecall, this guest returns 0 from main (the ELF entry, via -e main). That's still a clean halt: ra/x1 starts at 0, the trailing ret sets pc = 0, and the executor treats pc == 0 as termination. After commit(&[1]) this is fine.
  • __getrandom_v03_custom deterministic fill (syscalls/src/random.rs): seeding from the fixed-seed StdRng is intentional and actually required for a provable/deterministic VM — it just unblocks std::HashMap's RandomState. The unsafe slice construction is correctly documented and getrandom guarantees a valid non-null dest_ptr. Insecurity is documented and acceptable in this context.
  • MAX_PRIVATE_INPUT_SIZE 6.7 MB → 64 MiB: all consumers reference the constant directly (executor store + the verifier's num_private_input_pages bound in prover/src/lib.rs), so the change stays consistent; the page bound auto-scales (~257 pages).

Medium — build/CI coupling worth confirming is intentional

compile-programs now depends on compile-recursion-elfs, and test-executor: compile-programs. Since the recursion guest depends on the whole lambda-vm-prover (→ stark/crypto/math), make test-executor (and any compile-programs) now compiles the entire STARK verifier for the RISC-V target via build-std. That's a notable new build-time cost and a new failure surface for a previously light target. If that coupling isn't desired, consider keeping compile-recursion-elfs out of compile-programs and only wiring it into test-prover-all (which already depends on it).

Low — stale/misleading comments (inline)

  • prover/src/lib.rs:962 "~26 pages" comment is now stale after the 64 MiB bump (flagged inline on memory.rs).
  • bench_vs/lambda/recursion/Cargo.toml comment claims default-features = false drops sysinfo, but sysinfo is an unconditional prover dependency and is not dropped (inline).

Minor

  • recursion_smoke_test.rs asserts blob.len() < MAX_PRIVATE_INPUT_SIZE (strict) while store_private_inputs allows <=; harmless off-by-one in a test guard.

Nice touch: switching the guest .elf rules to FORCE fixes a latent bug where editing src/*.rs (only Cargo.toml was a prereq) wouldn't trigger a rebuild.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Benchmark — ethrex 20 transfers (median of 3)

Table parallelism: auto (cores / 3)

Metric main PR Δ
Peak heap 72950 MB 72558 MB -392 MB (-0.5%) ⚪
Prove time 40.160s 40.140s -0.020s (+-0.0%) ⚪

✅ No significant change.

✅ Low variance (time: 1.3%, heap: 1.7%)

Commit: 90a942f · Baseline: cached · Runner: self-hosted bench

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

AI Review

PR #742 · 17 changed files

Findings

Status Sev Location Finding Found by
candidate medium executor/src/vm/memory.rs:48 MAX_PRIVATE_INPUT_SIZE 10x bump dramatically increases executor/prover memory footprint minimax
minimax/MiniMax-M3
candidate medium executor/tests/rust.rs:203 Magic-number expected output in test_random moonmath
zro/minimax-m3
candidate low Makefile:119 FORCE dependency forces cargo re-run for every make invocation moonmath
zro/minimax-m3
candidate low bench_vs/lambda/recursion/Cargo.toml:9 Misleading comment about default-features dropping sysinfo moonmath
zro/minimax-m3
candidate low executor/src/vm/memory.rs:206 commit_public_output reports CommitSizeExceeded on u64 length overflow minimax
minimax/MiniMax-M3
candidate low executor/tests/rust.rs:203 test_random asserts a hard-coded byte that depends on rand internals minimax
minimax/MiniMax-M3
candidate low prover/src/lib.rs:968 num_private_input_pages max bound formula is off by ~2 minimax
minimax/MiniMax-M3
candidate low syscalls/Cargo.toml:11 Dead/unused getrandom_v2 dependency in syscalls moonmath
zro/minimax-m3
candidate low syscalls/src/random.rs:51 Custom getrandom backend assumes single-threaded + std::sync::Mutex under lower-atomic moonmath
zro/minimax-m3

Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro).

AI-001: MAX_PRIVATE_INPUT_SIZE 10x bump dramatically increases executor/prover memory footprint
  • Status: candidate
  • Severity: medium
  • Location: executor/src/vm/memory.rs:48
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

The bump from 6.7 MiB to 64 MiB makes Memory::set_bytes_aligned insert 16M entries into a U64HashMap&lt;[u8;4]&gt;, the prover's build_initial_image allocate a 64M-entry HashMap, and the prover's PagedMem allocate 256+ pages × 256 KiB of MemoryCell data, with no analysis of whether default CI runners can hold that memory.

Evidence

executor/src/vm/memory.rs:48 sets MAX_PRIVATE_INPUT_SIZE=6410241024; memory.rs:258-273 set_bytes_aligned inserts one HashMap entry per 4-byte chunk (16M for 64 MiB); prover/src/tables/trace_builder.rs:1856 build_initial_image builds a HashMap<u64,u8> with one entry per byte; prover/src/paged_mem.rs:43-46 PagedMem stores (u64, Page<T>) with Page.data sized DEFAULT_PAGE_SIZE.

Suggested fix

Document expected memory usage, gate recursion on a runtime/CI knob, or process private input page-by-page through set_bytes_aligned instead of inserting all 16M cells into one HashMap.

AI-002: Magic-number expected output in test_random
  • Status: candidate
  • Severity: medium
  • Location: executor/tests/rust.rs:203
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

The test now hard-codes vec![116] as the expected committed byte from rand::random::&lt;u8&gt;(), but no comment justifies the value. It silently relies on the first byte of StdRng::seed_from_u64(0x1234567890abcdef) being 116, which is brittle — any change to the StdRng algorithm, the seed, the rand crate version, or the order of rand::random calls vs other getrandom consumers will break the test with a confusing magic-number mismatch.

Evidence

executor/programs/rust/random/src/main.rs calls rand::random::&lt;u8&gt;() once then commit(&amp;x.to_le_bytes()). The randomness comes from __getrandom_v03_custom in syscalls/src/random.rs, which uses StdRng::seed_from_u64(RANDOM_SEED = 0x1234567890abcdef). The test asserts the first byte is 116 with no rationale. The previous test asserted a specific panic message instead of a magic value.

Suggested fix

Either add a brief comment explaining that 116 is the first byte of StdRng with the documented seed (and pin the rand version + StdRng algorithm in a comment), or replace the magic number with RANDOM_FIRST_BYTE exported from syscalls so the test and the implementation share the source of truth. Also consider asserting a short prefix range (e.g., a single bit pattern) rather than a single concrete byte if the goal is only "is the backend wired up and deterministic".

AI-003: FORCE dependency forces cargo re-run for every make invocation
  • Status: candidate
  • Severity: low
  • Location: Makefile:119
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

All three guest build rules (rust programs, bench, recursion-elfs) declare FORCE as a prerequisite, so cargo is invoked on every make invocation even when nothing changed. Cargo's incremental machinery handles the "no rebuild needed" case, but the cargo metadata/startup cost is paid each time, which adds latency to every make test and make compile-programs run.

Evidence

Makefile lines 119, 123, 145-149 all use FORCE as a normal prereq for the %.elf pattern rules. .PHONY: FORCE (line 134) means FORCE is always considered out of date, so the recipes always run. The comment on lines 132-136 acknowledges this and says it's intentional.

Suggested fix

This is a deliberate trade-off — flagging only because it adds ~cargo-startup seconds to every CI invocation that runs make test. If the cost becomes meaningful, consider moving the cargo build invocation under a stamp-file sentinel (touch a .cargo-stamp only when cargo actually rebuilt something) to short-circuit the common case where nothing changed.

AI-004: Misleading comment about default-features dropping sysinfo
  • Status: candidate
  • Severity: low
  • Location: bench_vs/lambda/recursion/Cargo.toml:9
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

The comment says default-features = false drops both parallel and sysinfo, but sysinfo is a regular non-optional dependency in prover/Cargo.toml that is always compiled regardless of the default features. Only parallel is actually gated by default = ["parallel"] in prover/Cargo.toml.

Evidence

prover/Cargo.toml lines 8 and 24: default = ["parallel"] only covers the parallel feature, and sysinfo = { version = "0.31", default-features = false, features = ["system"] } is declared unconditionally with no feature gate. The recursion guest at bench_vs/lambda/recursion/Cargo.toml line 13 imports the prover with default-features = false, but the comment on lines 9-12 wrongly claims sysinfo is also dropped.

Suggested fix

Correct the comment to state that only parallel is dropped and that the auto_storage module that references sysinfo is disk-spill-gated (so sysinfo is pulled in as a dep but never referenced at runtime in the guest).

AI-005: commit_public_output reports CommitSizeExceeded on u64 length overflow
  • Status: candidate
  • Severity: low
  • Location: executor/src/vm/memory.rs:206
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

checked_add(length).ok_or(MemoryError::CommitSizeExceeded)? collapses a u64 overflow into CommitSizeExceeded, which is the same variant the cap check returns — semantically wrong (it's not a commit-size violation, it's a numeric overflow) and hides the real cause from callers/observers.

Evidence

executor/src/vm/memory.rs:206-211: checked_add(length).ok_or(MemoryError::CommitSizeExceeded)? followed by if new_total &gt; MAX_PUBLIC_OUTPUT_TOTAL_SIZE { return Err(CommitSizeExceeded); }; MemoryError has only AddressOverflow/CommitSizeExceeded/PrivateInputSizeExceeded/UnalignedAccess/AllocationFailed — overflow on the running-total add cannot propagate as AddressOverflow because AddressOverflow is raised only in load_bytes.

Suggested fix

Either drop the checked_add (the next &gt; check would still catch huge values up to u64::MAX) or map the overflow to AddressOverflow/AllocationFailed to keep semantics honest.

AI-006: test_random asserts a hard-coded byte that depends on rand internals
  • Status: candidate
  • Severity: low
  • Location: executor/tests/rust.rs:203
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

run_program_and_check_public_output("./program_artifacts/rust/random.elf", vec![116], vec![]) ties the test to the exact value rand 0.9.2 returns when its thread_rng first calls __getrandom_v03_custom. Any rand upgrade, buffer-size change, or swap of thread_rng (e.g., SmallRng -> StdRng) silently breaks the assertion.

Evidence

executor/tests/rust.rs:203 expects vec![116]; syscalls/src/random.rs:53 fills via rng.fill_bytes(dest) from a StdRng seeded with a fixed seed, but the byte that ends up in random::&lt;u8&gt;() depends on rand's internal buffer size and which slot it returns; syscalls/Cargo.toml pulls rand = "0.9.2".

Suggested fix

Either assert a property (e.g., the same value across two runs) or pin a comment marking the value as rand-version-dependent and re-verify on upgrades; better, replace the deterministic backend with a counter-based one whose output the test can read directly.

AI-007: num_private_input_pages max bound formula is off by ~2
  • Status: candidate
  • Severity: low
  • Location: prover/src/lib.rs:968
  • Found by: minimax:minimax/MiniMax-M3
  • Verified by: -
  • Rejected by: -

Claim

The verifier's bound (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(DEFAULT_PAGE_SIZE) + 1 returns 258 for MAX=64 MiB (page size 256 KiB) when the prover actually creates 256 pages. The bound is loose (safe direction) but the formula was not refreshed when MAX changed and now admits two extra phantom pages, so a malicious proof can claim an inflated num_private_input_pages within a wider window than necessary.

Evidence

prover/src/lib.rs:966-975 uses (MAX_PRIVATE_INPUT_SIZE+4).div_ceil(DEFAULT_PAGE_SIZE)+1; executor/src/vm/memory.rs:48 sets MAX=6410241024, plus 4-byte length prefix, spans 256 pages (256 KiB each); prover/src/tables/trace_builder.rs:3877 pushes exactly num_private_input_pages PageConfigs starting at page_base_for_address(PRIVATE_INPUT_START_INDEX).

Suggested fix

Use the exact page count ((MAX_PRIVATE_INPUT_SIZE + DEFAULT_PAGE_SIZE - 1).div_ceil(DEFAULT_PAGE_SIZE)) or compute it from PRIVATE_INPUT_START_INDEX + MAX_PRIVATE_INPUT_SIZE + 4 directly.

AI-008: Dead/unused getrandom_v2 dependency in syscalls
  • Status: candidate
  • Severity: low
  • Location: syscalls/Cargo.toml:11
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

getrandom_v2 = {version = "0.2.15", features = ["custom"], package = "getrandom"} is declared but never imported or referenced anywhere in the syscalls crate's source code. It compiles into the dependency tree for every guest for no purpose.

Evidence

Grep across syscalls/src/*.rs finds no use getrandom_v2 and no extern crate getrandom_v2. The crate's only direct getrandom reference is use getrandom::Error in random.rs, which points at the v0.3 dep on line 10. v2's __getrandom_v02_custom symbol is never defined either (only __getrandom_v03_custom is).

Suggested fix

Delete the getrandom_v2 line from syscalls/Cargo.toml. If it was kept intentionally as scaffolding for a future v0.2 backend, leave a TODO comment — but as written it is just dead weight in the dep graph.

AI-009: Custom getrandom backend assumes single-threaded + std::sync::Mutex under lower-atomic
  • Status: candidate
  • Severity: low
  • Location: syscalls/src/random.rs:51
  • Found by: moonmath:zro/minimax-m3
  • Verified by: -
  • Rejected by: -

Claim

__getrandom_v03_custom takes the std::sync::Mutex around the shared RNG inside a guest compiled with -C passes=lower-atomic. std::sync::Mutex uses atomic operations internally for its synchronization; lowering those is fine because the target is singlethread: true, but the choice is fragile — if any future guest enables threads (or the lowering pass is removed) the lock silently becomes unsound.

Evidence

syscalls/src/random.rs line 51 locks std::sync::Mutex&lt;StdRng&gt;; the .cargo/config.toml files in bench_vs/lambda/recursion/ and other guests all set -C passes=lower-atomic. The target spec executor/programs/riscv64im-lambda-vm-elf.json sets singlethread: true, which makes this safe today. There is no comment warning that the lower-atomic + singlethread combination is what makes the std::sync::Mutex sound here.

Suggested fix

Add a brief comment noting that the lock is only sound because the target is single-threaded and atomics are lowered; or use a RefCell&lt;StdRng&gt; to make the single-threadedness an enforced invariant at compile time.

Reviewer Lanes

Lane Model Prompt Status Findings
glm openrouter/z-ai/glm-5.2 general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
kimi openrouter/moonshotai/kimi-k2.7-code general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0
minimax minimax/MiniMax-M3 general success 4
moonmath zro/minimax-m3 general success 5
nemotron openrouter/nvidia/nemotron-3-ultra-550b-a55b general error: opencode failed (provider/auth/runtime error) and no findings were submitted 0

Verification Lanes

Lane Model Status Confirmed Rejected Uncertain
deepseek-verifier openrouter/deepseek/deepseek-v4-pro error: opencode failed (provider/auth/runtime error) and no verifications were submitted 0 0 0

Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report.

Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts.

@Oppen Oppen force-pushed the pr/recursion-guest branch from fcf20ef to c8c440d Compare June 30, 2026 14:24
@Oppen

Oppen commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

/ai-review

@Oppen Oppen force-pushed the pr/recursion-guest branch from c8c440d to a74abf7 Compare June 30, 2026 14:25
@github-actions

Copy link
Copy Markdown

Codex Code Review

Finding

  • High: syscalls/src/random.rs:48 emits print_string from the new getrandom v0.3 backend. Print ecalls execute, but the prover’s ECALL bus is only balanced by supported proof syscalls like Commit and Halt; there is no receiver for syscall 1. Any guest that calls this backend during a proved run can execute successfully and then fail proving/verification. This is especially likely for the new recursion guest because verify_with_options uses standard-library structures that can initialize randomized hash state. Remove these print_string calls from proofable guest paths, or add a proven Print syscall if printing must be supported.

I did not run builds or tests, per the static-review constraints.

Comment thread Makefile
Comment thread bench_vs/lambda/recursion/Cargo.toml Outdated
@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review: feat: enable recursion

Solid, well-documented experimental enablement PR. No safety/memory-safety issues found in the Rust/unsafe changes (the getrandom backend and the larger private-input region are both sound and necessary). A few notes below; nothing blocking.

Findings

Build cost (Medium) — see inline on Makefile
compile-programs now depends on compile-recursion-elfs, so test-executor and test will build the full in-VM verifier (entire prover crate-graph via build-std) even though only the ignored prover smoke tests use those ELFs. test-prover-all already pulls them in. Suggest removing from compile-programs.

Design / soundness note (Low, expected for "naive" recursion)
The recursion guest commits only [1] on success. It binds nothing about what it verified to the outer proof's public output: the inner ELF, the ProofOptions (which come from untrusted private input — a prover can pick weak params; the tests themselves use blowup=2, fri_queries=1), and the inner proof's public I/O. So the outer proof attests "I verified some inner proof under some options," which isn't yet meaningful recursion. Fine as a verifier-cost benchmark (the stated goal), but real recursion will need to commit at least the inner ELF hash + options + inner public I/O. Worth a guest comment noting this is intentionally not sound recursion yet.

Stale comment (Low) — prover/src/lib.rs:962
// MAX_PRIVATE_INPUT_SIZE fits in ~26 pages of DEFAULT_PAGE_SIZE. — with the new 64 MiB constant and DEFAULT_PAGE_SIZE = 256 KiB, that's now ~256 pages. The max_pages bound is derived from the constant so it stays correct; only the comment is stale.

Comment accuracy (Low) — see inline on recursion/Cargo.toml
default-features = false does not drop sysinfo (non-optional dep); it only disables parallel/rayon.

Looked at and OK

  • MAX_PRIVATE_INPUT_SIZE → 64 MiB: region 0xFF000000 + 64 MiB stays well below the stack (top of 64-bit space) and the page-count checks in verify_with_options auto-scale. Worst-case input now materializes ~16M memory cells; the test comments already call out the large RAM footprint.
  • random.rs: routing getrandom to the constant-seeded StdRng (vs. panic) is correct for a deterministic VM — guest-side and fully provable; insecurity is documented.
  • Makefile FORCE prereqs fix a latent staleness bug (guest .elfs previously only rebuilt on Cargo.toml changes, not src/ changes).

- Introduces smoke test with verification of empty program
- Implements `__getrandom_v03_custom` to avoid panics when accessing
  hashmaps
- Updated test to expect deterministic output by inspecting the current
  output
@Oppen Oppen force-pushed the pr/recursion-guest branch from a74abf7 to 90a942f Compare June 30, 2026 14:48
@Oppen

Oppen commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

/ai-review

@Oppen

Oppen commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

/bench

@Oppen Oppen marked this pull request as ready for review June 30, 2026 14:51
@github-actions

Copy link
Copy Markdown

Codex Code Review

Findings

Medium - Security: syscalls/src/random.rs now makes getrandom succeed with bytes from a fixed seed. That silently changes the standard randomness contract: any guest dependency using rand/getrandom for keys, nonces, salts, or randomized algorithms now receives predictable bytes while the API reports success. The runtime warning is also effectively invisible in provable guests because print_string is a no-op. Keep deterministic behavior only for explicitly non-security use cases, or make this backend fail unless callers opt into weak deterministic randomness.

Medium - Performance/CI: Makefile adds compile-recursion-elfs to the general compile-programs target, so make test and make test-executor now build the recursion verifier guest even though the recursion tests are ignored and not used by executor tests. That guest pulls in the prover and is much heavier than normal test ELFs. This should stay behind test-prover-all or a dedicated recursion target.

Low - Performance: Makefile changes all existing Rust guest artifact rules to depend on FORCE, so every make compile-programs-rust, make compile-bench, and dependent test target shells into every guest crate and runs cargo build plus cp even when the .elf artifacts are current. Cargo may no-op internally, but this still removes Make’s incremental skip for a large set of guests. Scope FORCE to the new recursion artifacts or add real source/lockfile dependencies for the existing rules.

I did not run builds or tests per the static-review constraints.

@claude

claude Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review: feat: enable recursion (#742)

Reviewed the full diff plus surrounding code (syscalls, executor memory layout, memory AIR). No critical/high/medium issues found — the change is well-scoped and notably well-documented. A few low-severity observations:

Low

  • syscalls/src/random.rs — getrandom now fails silently-insecure instead of loud. __getrandom_v03_custom changes from panic!("getrandom is not supported") to returning deterministic bytes from a fixed-seed StdRng. This is correct and necessary (std HashMap's RandomState in the verifier guest), and it's scoped to guests compiled with getrandom_backend="custom" (today: random, recursion). The tradeoff is that any future guest opting into the custom backend silently gets predictable randomness rather than a hard failure — fine for a zkVM where randomness must be deterministic anyway, but worth keeping in mind. The added print_string calls are no-ops (syscalls.rs:32), so no Print-ecall bus risk.

  • Makefilecompile-recursion-elfs rebuilds fibonacci a second time into program_artifacts/recursion/ (it's already built by compile-bench). Intentional for artifact-dir isolation, just minor duplicate build work.

  • Test coupling to exact RNG output (executor/tests/rust.rs expecting [116], recursion_smoke_test.rs MIN_PROOF_OPTIONS). Deterministic only because Cargo.lock pins rand/StdRng; a future rand bump could shift the byte. Acceptable.

Verified clean

  • 64 MiB private input crosses the 4 GiB boundary (0xFF0000000x103000000), but the memory AIR carries address_hi/address_lo 32-bit columns (full 64-bit addresses), the boundary is page-aligned, and the guest heap caps at 0xC0000000 — no overlap, no truncation.
  • Shared RNG mutex between sys_rand and the getrandom backend stays deterministic (single-threaded guest, fixed call order) → sound for proving.
  • Re-export of ProofOptions, postcard (VmProof, Vec<u8>, ProofOptions) round-trip, and the panic-hook→sys_panic install in the guest all look correct.
  • FORCE-based .elf rules now correctly rebuild when guest sources change (previously only Cargo.toml was a prereq) — an improvement, with cargo deciding the actual no-op.

The recursion tests are all #[ignore]d and gated on prebuilt ELFs, so they don't burden default CI. LGTM.

Comment thread bench_vs/lambda/recursion/Cargo.toml Outdated
Comment thread executor/src/vm/memory.rs Outdated
Oppen and others added 3 commits June 30, 2026 14:31
- Collapse the three byte-identical guest-ELF pattern recipes (rust, bench,
  recursion) into a single `build_guest_elf` canned recipe parameterized by
  source dir ($1) and built-binary name suffix ($2). The cargo invocation was
  identical across all three; only the directory and the `-bench` copy suffix
  differed. Verified with `make -n` that the rust/bench rules still copy
  `release/<stem>` and the recursion rule copies `release/<stem>-bench`.

- Clarify the compile-programs NOTE: the recursion smoke tests are #[ignore]d
  (not run by `make test`/`test-executor`, only `test-prover-all`), but their
  guest ELFs are still compiled on every build so they keep building.

- recursion_smoke_test: the pre-check asserted `blob.len() < MAX_PRIVATE_INPUT_SIZE`
  while the executor rejects only `len > MAX` (memory.rs:228), so a blob exactly
  at the cap is accepted by the VM but would have failed the test guard. Use `<=`.
Makefile cleanup + recursion test guard fix (follow-ups to #742)
@MauroToscano MauroToscano added this pull request to the merge queue Jun 30, 2026
Merged via the queue into main with commit 912b443 Jun 30, 2026
19 checks passed
@MauroToscano MauroToscano deleted the pr/recursion-guest branch June 30, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants