44//! 1. Proves an inner program on the host.
55//! 2. Serializes `(VmProof, inner_elf, opts)` with postcard.
66//! 3. Hands that as private input to the recursion guest.
7- //! 4. Either **proves** the recursion guest's execution and verifies the outer
8- //! proof (`OuterMode::Prove`), or merely **executes** the guest in-VM and
9- //! reads the committed marker off the trace (`OuterMode::ExecuteOnly`) — a
10- //! cheaper tier that skips the LDE/FRI that dominate the full pipeline.
7+ //! 4. Either **proves** the recursion guest's execution (memory-bounded via
8+ //! continuations) and verifies the outer proof (`OuterMode::Prove`), or
9+ //! merely **executes** the guest in-VM and reads the committed marker
10+ //! straight off the executor's memory (`OuterMode::ExecuteOnly`) — a cheaper
11+ //! tier that skips the LDE/FRI that dominate the full pipeline.
1112//!
12- //! The guest ELFs are built by `make compile-recursion-elfs` (which the
13- //! `test-prover-all` make target depends on) and read from
14- //! `executor/program_artifacts/recursion/`, like every other program test.
15- //!
16- //! Tests are `#[ignore]`d because the outer proof runs the full STARK verifier
17- //! inside the VM (minutes per run, large memory footprint).
13+ //! The guest ELFs are assumed built by `make compile-recursion-elfs`.
1814
1915use std:: path:: PathBuf ;
2016
@@ -83,64 +79,77 @@ fn prove_inner_and_encode_blob(
8379/// whether we also prove the guest's own execution.
8480#[ derive( Clone , Copy , Debug ) ]
8581enum OuterMode {
86- /// Execute the guest in-VM and read the committed marker off the trace.
87- /// Skips the LDE blowup + FRI commit that dominate the full pipeline's
88- /// footprint, so it needs materially less RAM than `Prove`.
89- ///
90- /// "Less" is not "little": `Executor::run` retains a per-instruction log
91- /// and `Traces` materializes the full execution trace, so verifying even a
92- /// 1-query inner proof still needs tens of GB — it OOMs on a 36 GB box.
82+ /// Execute the guest in-VM and read the committed marker straight off the
83+ /// executor's memory. Streams logs via `Executor::resume()` and never
84+ /// builds a `Traces`, so footprint stays bounded to the VM's touched
85+ /// memory + instruction cache. Skips the LDE/FRI of the full pipeline entirely.
9386 ExecuteOnly ,
94- /// Prove the guest's execution and verify the outer proof on the host. The
95- /// full STARK verifier inside the VM — minutes per run, ~125 GB.
87+ /// Prove the guest's execution via continuations, then verify the outer
88+ /// proof on the host. `prove_and_verify_continuation` retains every epoch's
89+ /// STARK proof in the bundle before verifying, so peak RAM grows with epoch
90+ /// count. Heavy — excluded from CI, run manually. A future verify-one-and-
91+ /// discard API extension would make this memory-friendlier.
9692 Prove ,
9793}
9894
9995/// Execute the recursion guest in-VM on `blob` and return the bytes it
10096/// committed (the success marker the in-VM verifier emits).
97+ ///
98+ /// Streams execution via `Executor::resume()`. The committed marker is
99+ /// read directly off the executor's memory. This avoids OOMs.
101100fn execute_outer_and_commit ( label : & str , recursion_elf_bytes : & [ u8 ] , blob : & [ u8 ] ) -> Vec < u8 > {
102101 use executor:: elf:: Elf ;
103102 use executor:: vm:: execution:: Executor ;
104103
105- eprintln ! ( "[{label}] executing outer (recursion guest, in-VM verify) ..." ) ;
104+ eprintln ! ( "[{label}] executing outer (recursion guest, in-VM verify, streaming ) ..." ) ;
106105 let program = Elf :: load ( recursion_elf_bytes) . expect ( "load recursion elf" ) ;
107- let result = Executor :: new ( & program, blob. to_vec ( ) )
108- . expect ( "executor new" )
109- . run ( )
110- . expect ( "recursion guest execution failed (verify panicked in-VM?)" ) ;
111-
112- let traces = crate :: tables:: trace_builder:: Traces :: from_elf_and_logs (
113- & program,
114- & result. logs ,
115- & crate :: MaxRowsConfig :: default ( ) ,
116- blob,
117- #[ cfg( feature = "disk-spill" ) ]
118- stark:: storage_mode:: StorageMode :: Ram ,
119- )
120- . expect ( "trace build" ) ;
106+ let mut executor = Executor :: new ( & program, blob. to_vec ( ) ) . expect ( "executor new" ) ;
107+
108+ // Drain chunks to completion without retaining logs or building a trace.
109+ while executor
110+ . resume ( )
111+ . expect ( "recursion guest execution failed (verify panicked in-VM?)" )
112+ . is_some ( )
113+ { }
114+
115+ let committed = executor
116+ . finish ( )
117+ . expect ( "read committed output after execution" )
118+ . memory_values ;
121119
122120 eprintln ! (
123121 "[{label}] committed {} bytes: {:?} (as str: {:?})" ,
124- traces . public_output_bytes . len( ) ,
125- traces . public_output_bytes ,
126- String :: from_utf8_lossy( & traces . public_output_bytes ) ,
122+ committed . len( ) ,
123+ committed ,
124+ String :: from_utf8_lossy( & committed ) ,
127125 ) ;
128- traces . public_output_bytes
126+ committed
129127}
130128
131- /// Prove the recursion guest's execution on `blob`, verify the outer proof on
132- /// the host, and return the bytes the guest committed.
133- fn prove_outer_and_commit ( label : & str , recursion_elf_bytes : & [ u8 ] , blob : & [ u8 ] ) -> Vec < u8 > {
134- eprintln ! ( "[{label}] proving outer (recursion guest) ..." ) ;
135- let outer_proof =
136- crate :: prove_with_inputs ( recursion_elf_bytes, blob) . expect ( "outer prove should succeed" ) ;
137- eprintln ! ( "[{label}] outer proof generated" ) ;
129+ /// Epoch size for the outer prove: 2^16 ≈ 65K cycles. Small so one epoch's
130+ /// trace+LDE stays under the ~16GiB CI runners.
131+ const OUTER_EPOCH_SIZE_LOG2 : u32 = 16 ;
138132
139- assert ! (
140- crate :: verify( & outer_proof, recursion_elf_bytes) . expect( "outer verify errored" ) ,
141- "outer proof must verify on host"
133+ /// Prove the recursion guest's execution on `blob` memory-bounded via
134+ /// continuations and verify the bundle on the host, returning the bytes the
135+ /// guest committed.
136+ fn prove_outer_and_commit ( label : & str , recursion_elf_bytes : & [ u8 ] , blob : & [ u8 ] ) -> Vec < u8 > {
137+ let opts =
138+ crate :: GoldilocksCubicProofOptions :: with_blowup ( 2 ) . expect ( "blowup=2 is always valid" ) ;
139+ eprintln ! (
140+ "[{label}] proving outer (recursion guest) via continuations \
141+ (epoch=2^{OUTER_EPOCH_SIZE_LOG2} cycles) ..."
142142 ) ;
143- outer_proof. public_output
143+ let committed = crate :: continuation:: prove_and_verify_continuation (
144+ recursion_elf_bytes,
145+ blob,
146+ OUTER_EPOCH_SIZE_LOG2 ,
147+ & opts,
148+ )
149+ . expect ( "outer continuation prove/verify errored" )
150+ . expect ( "outer continuation proof must verify on host" ) ;
151+ eprintln ! ( "[{label}] outer continuation proof generated and verified" ) ;
152+ committed
144153}
145154
146155/// Core pipeline: prove an inner program with the given options, hand the
@@ -214,10 +223,7 @@ fn run_recursion_pipeline(
214223
215224/// Reproduce the recursion guest's EXACT path on the host — decode the postcard
216225/// blob into `(VmProof, Vec<u8>, ProofOptions)` and call `verify_with_options`.
217- /// The cheapest regression guard in this file: no VM execution, just the
218- /// encode/decode contract plus a host verify, so it catches drift in the proof
219- /// format or the blob layout in seconds. Unlike the guest, a failure here
220- /// surfaces the actual error instead of an infinite abort loop.
226+ /// Cheap regression guard.
221227#[ test]
222228#[ ignore = "needs prebuilt guest ELF (make compile-recursion-elfs)" ]
223229fn test_recursion_blob_decodes_and_verifies_on_host ( ) {
@@ -249,15 +255,11 @@ fn test_recursion_blob_decodes_and_verifies_on_host() {
249255}
250256
251257// === Execute-only tier ========================================================
252- // Mirrors the proving tests below, but stops at `OuterMode::ExecuteOnly`: the
253- // guest runs in-VM and we read the committed marker off the trace, skipping the
254- // outer STARK prove. Needs tens of GB (execution trace), not the ~125 GB the
255- // full outer prove wants — but still OOMs on a 36 GB box.
256258
257259/// Execute-only mirror of `test_recursion_prove_empty`: verify a `blowup=8`
258260/// proof of the empty program in-VM.
259261#[ test]
260- #[ ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace )" ]
262+ #[ ignore = "slow: runs the in-VM STARK verifier (minutes on CI )" ]
261263fn test_recursion_execute_empty ( ) {
262264 let root = workspace_root ( ) ;
263265 let empty_elf_bytes = read_guest_elf ( & root, "empty" ) ;
@@ -272,7 +274,7 @@ fn test_recursion_execute_empty() {
272274/// Execute-only mirror of `test_recursion_prove_1query`: smallest possible
273275/// inner proof (blowup=2, 1 query) → least guest work.
274276#[ test]
275- #[ ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace )" ]
277+ #[ ignore = "slow: runs the in-VM STARK verifier (minutes on CI )" ]
276278fn test_recursion_execute_1query ( ) {
277279 let root = workspace_root ( ) ;
278280 let empty_elf_bytes = read_guest_elf ( & root, "empty" ) ;
@@ -288,7 +290,7 @@ fn test_recursion_execute_1query() {
288290/// Execute-only mirror of `test_recursion_prove`: verify a `blowup=8` proof of
289291/// fibonacci(10) in-VM.
290292#[ test]
291- #[ ignore = "needs prebuilt recursion guest ELF + tens of GB RAM (execution trace )" ]
293+ #[ ignore = "slow: runs the in-VM STARK verifier (minutes on CI )" ]
292294fn test_recursion_execute ( ) {
293295 let root = workspace_root ( ) ;
294296 let fib_elf_bytes = read_guest_elf ( & root, "fibonacci" ) ;
@@ -307,10 +309,9 @@ fn test_recursion_execute() {
307309// === Full-prove tier ==========================================================
308310
309311/// Inner program: empty (halt immediately). Useful for measuring the
310- /// lambda-vm verifier's intrinsic recursion overhead — i.e. what it costs
311- /// to verify the smallest possible lambda-vm proof, with no inner workload.
312+ /// verifier's intrinsic recursion overhead.
312313#[ test]
313- #[ ignore = "slow: runs the full STARK verifier inside the VM" ]
314+ #[ ignore = "slow: memory-bounded continuation prove of the verifier-in- VM" ]
314315fn test_recursion_prove_empty ( ) {
315316 let root = workspace_root ( ) ;
316317 let empty_elf_bytes = read_guest_elf ( & root, "empty" ) ;
@@ -323,11 +324,9 @@ fn test_recursion_prove_empty() {
323324}
324325
325326/// Inner program: empty, but with the absolute-minimum FRI parameters
326- /// (blowup=2, **fri_number_of_queries=1**). This is a "can the pipeline even
327- /// run end-to-end on a 125 GB box" experiment — security is intentionally
328- /// terrible. Use only for capacity probing.
327+ /// (blowup=2, **fri_number_of_queries=1**). For quick profiling only.
329328#[ test]
330- #[ ignore = "slow: runs the full STARK verifier inside the VM" ]
329+ #[ ignore = "slow: memory-bounded continuation prove of the verifier-in- VM" ]
331330fn test_recursion_prove_1query ( ) {
332331 let root = workspace_root ( ) ;
333332 let empty_elf_bytes = read_guest_elf ( & root, "empty" ) ;
@@ -343,7 +342,7 @@ fn test_recursion_prove_1query() {
343342
344343/// Inner program: fibonacci(10).
345344#[ test]
346- #[ ignore = "slow: runs the full STARK verifier inside the VM" ]
345+ #[ ignore = "slow: memory-bounded continuation prove of the verifier-in- VM" ]
347346fn test_recursion_prove ( ) {
348347 let root = workspace_root ( ) ;
349348 let fib_elf_bytes = read_guest_elf ( & root, "fibonacci" ) ;
0 commit comments