@@ -100,15 +100,41 @@ pub struct ProofUploadSession {
100100 pub expires_at_slot : u64 ,
101101 /// Rolling SHA-256 over `(DOMAIN_TAG ‖ session_id ‖ total_len_le ‖ proof_system_id ‖ chunk_0 ‖ chunk_1 ‖ …)`.
102102 pub rolling_hash : [ u8 ; 32 ] ,
103+ /// Chunked-verification progress for systems whose single-shot
104+ /// verify exceeds the 1.4M CU per-transaction cap (FRI-STARK, #76).
105+ /// `Default` (all zero / false) until `stark_verify_begin` runs.
106+ pub stark_verify : StarkVerifyProgress ,
103107 /// Future-compat padding. Bumping `layout_version` takes from here first.
104- pub reserved : [ u8 ; 32 ] ,
108+ pub reserved : [ u8 ; 26 ] ,
105109 /// Assembled proof bytes (length grows with each `append_chunk`).
106110 pub assembled : Vec < u8 > ,
107111}
108112
113+ /// Resumable verification cursor for chunked STARK verification (#76).
114+ ///
115+ /// A production STARK proof exceeds the per-transaction CU cap, so its
116+ /// verification is split across instructions: one `begin` step runs the
117+ /// once-per-proof setup (`FriStark::verify_setup`) and records
118+ /// `num_queries`; subsequent steps each verify a contiguous query range
119+ /// (`FriStark::verify_query_range`) and advance `next_query`. When
120+ /// `next_query == num_queries` the proof is fully verified.
121+ #[ derive( Debug , Clone , Copy , Default , BorshSerialize , BorshDeserialize , PartialEq , Eq ) ]
122+ pub struct StarkVerifyProgress {
123+ /// `true` once the setup step (shape + PoW + OOD) has passed.
124+ pub setup_done : bool ,
125+ /// Next query index still to verify. Advances monotonically to
126+ /// `num_queries`.
127+ pub next_query : u16 ,
128+ /// Total query count, recorded by the setup step.
129+ pub num_queries : u16 ,
130+ /// `true` once every query has been verified.
131+ pub complete : bool ,
132+ }
133+
109134impl ProofUploadSession {
110135 /// Current account layout version. Bump to perform a migration.
111- pub const LAYOUT_VERSION : u8 = 1 ;
136+ /// v2 added the `stark_verify` progress cursor (#76).
137+ pub const LAYOUT_VERSION : u8 = 2 ;
112138
113139 /// Maximum borsh-serialized length of the fixed-size header.
114140 /// Computed at compile time so handlers can size accounts correctly.
@@ -125,7 +151,8 @@ impl ProofUploadSession {
125151 + 8 // created_at_slot
126152 + 8 // expires_at_slot
127153 + 32 // rolling_hash
128- + 32 // reserved
154+ + 6 // stark_verify (1 + 2 + 2 + 1)
155+ + 26 // reserved
129156 + 4 ; // borsh Vec<u8> length prefix for `assembled`
130157
131158 /// Total serialized account size for a session declaring `total_len`
@@ -171,7 +198,8 @@ impl ProofUploadSession {
171198 created_at_slot,
172199 expires_at_slot : created_at_slot. saturating_add ( EXPIRY_SLOTS ) ,
173200 rolling_hash : h_0,
174- reserved : [ 0 ; 32 ] ,
201+ stark_verify : StarkVerifyProgress :: default ( ) ,
202+ reserved : [ 0 ; 26 ] ,
175203 assembled : Vec :: with_capacity ( total_len as usize ) ,
176204 }
177205 }
@@ -246,6 +274,73 @@ impl ProofUploadSession {
246274 Ok ( ( ) )
247275 }
248276
277+ /// Begin chunked STARK verification (#76): record the once-per-proof
278+ /// setup result. The caller must have run
279+ /// `FriStark::verify_setup(...)` (shape + PoW + OOD) successfully and
280+ /// pass the returned `num_queries`. Requires a finalized session.
281+ ///
282+ /// Idempotent on re-call with the same `num_queries` (a retried
283+ /// transaction does not corrupt the cursor); a different
284+ /// `num_queries` is rejected.
285+ ///
286+ /// # Errors
287+ ///
288+ /// - [`OnChainError::SessionNotFinalized`] — proof bytes incomplete.
289+ /// - [`OnChainError::StarkVerifyOutOfOrder`] — re-begun with a
290+ /// different `num_queries` after queries were already verified.
291+ pub fn stark_verify_begin ( & mut self , num_queries : u16 ) -> Result < ( ) , OnChainError > {
292+ if !self . finalized {
293+ return Err ( OnChainError :: SessionNotFinalized ) ;
294+ }
295+ if self . stark_verify . setup_done {
296+ if self . stark_verify . num_queries != num_queries {
297+ return Err ( OnChainError :: StarkVerifyOutOfOrder ) ;
298+ }
299+ return Ok ( ( ) ) ;
300+ }
301+ self . stark_verify = StarkVerifyProgress {
302+ setup_done : true ,
303+ next_query : 0 ,
304+ num_queries,
305+ complete : num_queries == 0 ,
306+ } ;
307+ Ok ( ( ) )
308+ }
309+
310+ /// Advance the chunked STARK verification cursor after a query range
311+ /// `[next_query, new_next)` verified successfully via
312+ /// `FriStark::verify_query_range(...)`. `new_next` must strictly
313+ /// advance the cursor and not overshoot `num_queries`. When it
314+ /// reaches `num_queries` the proof is marked complete.
315+ ///
316+ /// # Errors
317+ ///
318+ /// - [`OnChainError::StarkVerifyNotStarted`] — `stark_verify_begin`
319+ /// has not run.
320+ /// - [`OnChainError::StarkVerifyOutOfOrder`] — `new_next` does not
321+ /// strictly advance the cursor or exceeds `num_queries`.
322+ pub fn stark_verify_advance ( & mut self , new_next : u16 ) -> Result < ( ) , OnChainError > {
323+ if !self . stark_verify . setup_done {
324+ return Err ( OnChainError :: StarkVerifyNotStarted ) ;
325+ }
326+ if new_next <= self . stark_verify . next_query || new_next > self . stark_verify . num_queries {
327+ return Err ( OnChainError :: StarkVerifyOutOfOrder ) ;
328+ }
329+ self . stark_verify . next_query = new_next;
330+ if new_next == self . stark_verify . num_queries {
331+ self . stark_verify . complete = true ;
332+ }
333+ Ok ( ( ) )
334+ }
335+
336+ /// `true` once every query has been verified through the chunked
337+ /// path. The driver checks this before treating the proof as
338+ /// accepted.
339+ #[ must_use]
340+ pub const fn stark_verify_complete ( & self ) -> bool {
341+ self . stark_verify . complete
342+ }
343+
249344 /// Record a verifier failure. Used after `finalize` succeeded but the
250345 /// verifier returned an error — the session stays open for cancellation.
251346 pub fn record_verify_failure ( & mut self , error_code : u32 ) {
@@ -309,6 +404,109 @@ mod tests {
309404 assert_eq ! ( session. assembled, alloc:: vec![ 1 , 2 , 3 , 4 ] ) ;
310405 }
311406
407+ fn finalized_session ( ) -> ProofUploadSession {
408+ let mut s = fixture_session ( 0 ) ;
409+ s. finalized = true ;
410+ s
411+ }
412+
413+ #[ test]
414+ fn stark_verify_begin_requires_finalized ( ) {
415+ let mut s = fixture_session ( 0 ) ;
416+ assert_eq ! (
417+ s. stark_verify_begin( 4 ) ,
418+ Err ( OnChainError :: SessionNotFinalized )
419+ ) ;
420+ }
421+
422+ #[ test]
423+ fn stark_verify_full_sequence ( ) {
424+ let mut s = finalized_session ( ) ;
425+ s. stark_verify_begin ( 4 ) . unwrap ( ) ;
426+ assert ! ( s. stark_verify. setup_done) ;
427+ assert_eq ! ( s. stark_verify. num_queries, 4 ) ;
428+ assert_eq ! ( s. stark_verify. next_query, 0 ) ;
429+ assert ! ( !s. stark_verify_complete( ) ) ;
430+
431+ s. stark_verify_advance ( 2 ) . unwrap ( ) ;
432+ assert_eq ! ( s. stark_verify. next_query, 2 ) ;
433+ assert ! ( !s. stark_verify_complete( ) ) ;
434+
435+ s. stark_verify_advance ( 4 ) . unwrap ( ) ;
436+ assert_eq ! ( s. stark_verify. next_query, 4 ) ;
437+ assert ! ( s. stark_verify_complete( ) ) ;
438+ }
439+
440+ #[ test]
441+ fn stark_verify_advance_before_begin_errors ( ) {
442+ let mut s = finalized_session ( ) ;
443+ assert_eq ! (
444+ s. stark_verify_advance( 1 ) ,
445+ Err ( OnChainError :: StarkVerifyNotStarted )
446+ ) ;
447+ }
448+
449+ #[ test]
450+ fn stark_verify_rejects_regress_and_overshoot ( ) {
451+ let mut s = finalized_session ( ) ;
452+ s. stark_verify_begin ( 4 ) . unwrap ( ) ;
453+ // not strictly advancing
454+ assert_eq ! (
455+ s. stark_verify_advance( 0 ) ,
456+ Err ( OnChainError :: StarkVerifyOutOfOrder )
457+ ) ;
458+ // overshoot num_queries
459+ assert_eq ! (
460+ s. stark_verify_advance( 5 ) ,
461+ Err ( OnChainError :: StarkVerifyOutOfOrder )
462+ ) ;
463+ s. stark_verify_advance ( 2 ) . unwrap ( ) ;
464+ // regress
465+ assert_eq ! (
466+ s. stark_verify_advance( 2 ) ,
467+ Err ( OnChainError :: StarkVerifyOutOfOrder )
468+ ) ;
469+ assert_eq ! (
470+ s. stark_verify_advance( 1 ) ,
471+ Err ( OnChainError :: StarkVerifyOutOfOrder )
472+ ) ;
473+ }
474+
475+ #[ test]
476+ fn stark_verify_begin_idempotent_same_count ( ) {
477+ let mut s = finalized_session ( ) ;
478+ s. stark_verify_begin ( 4 ) . unwrap ( ) ;
479+ s. stark_verify_advance ( 2 ) . unwrap ( ) ;
480+ // re-begin with same count is a no-op (retried tx), cursor kept
481+ s. stark_verify_begin ( 4 ) . unwrap ( ) ;
482+ assert_eq ! ( s. stark_verify. next_query, 2 ) ;
483+ // re-begin with a different count is rejected
484+ assert_eq ! (
485+ s. stark_verify_begin( 8 ) ,
486+ Err ( OnChainError :: StarkVerifyOutOfOrder )
487+ ) ;
488+ }
489+
490+ #[ test]
491+ fn stark_verify_zero_queries_complete_immediately ( ) {
492+ let mut s = finalized_session ( ) ;
493+ s. stark_verify_begin ( 0 ) . unwrap ( ) ;
494+ assert ! ( s. stark_verify_complete( ) ) ;
495+ }
496+
497+ #[ test]
498+ fn session_borsh_roundtrip_carries_stark_verify ( ) {
499+ let mut s = finalized_session ( ) ;
500+ s. stark_verify_begin ( 7 ) . unwrap ( ) ;
501+ s. stark_verify_advance ( 3 ) . unwrap ( ) ;
502+ let bytes = borsh:: to_vec ( & s) . unwrap ( ) ;
503+ let back = ProofUploadSession :: try_from_slice ( & bytes) . unwrap ( ) ;
504+ assert_eq ! ( s, back) ;
505+ assert_eq ! ( back. stark_verify. next_query, 3 ) ;
506+ assert_eq ! ( back. stark_verify. num_queries, 7 ) ;
507+ assert_eq ! ( back. layout_version, 2 ) ;
508+ }
509+
312510 #[ test]
313511 fn rejects_overflow ( ) {
314512 let mut session = fixture_session ( 2 ) ;
0 commit comments