Skip to content

Commit 8f58709

Browse files
committed
feat(chunked): StarkVerifyProgress checkpoint state machine (#76)
The on-chain half of chunked STARK verification: a resumable cursor in the session PDA that drives FriStark::verify_setup + verify_query_range across multiple instructions. ProofUploadSession (layout v1 -> v2) gains a StarkVerifyProgress field (setup_done, next_query, num_queries, complete), carved from the reserved padding so the account size is unchanged (reserved 32 -> 26 + 6 bytes of cursor). Three state-machine methods: stark_verify_begin(num_queries) Records the setup result; requires a finalized session. Idempotent on retry with the same count; rejects a different count after progress (StarkVerifyOutOfOrder). stark_verify_advance(new_next) Advances the cursor after a query range verified; must strictly advance and not overshoot num_queries. Marks complete at the end. stark_verify_complete() -> bool The driver's accept gate. The cursor enforces that the full query set [0, num_queries) is covered contiguously and exactly once, and that setup ran before any query step -- so a malicious driver cannot skip the PoW/OOD gate or a query batch and still reach `complete`. 3 new OnChainError variants: SessionNotFinalized (0x37), StarkVerifyNotStarted (0x38), StarkVerifyOutOfOrder (0x39). 7 host unit tests: finalized-gate, full begin->advance->complete sequence, advance-before-begin, regress/overshoot/empty rejection, begin idempotency, zero-query immediate completion, and a borsh roundtrip proving the v2 layout serializes the cursor. mosaic-chunked 27 + mosaic-core 16 tests green; workspace check clean (no error exhaustiveness breaks). Next: the mosaic-program instruction dispatch (BeginStarkVerify / StarkVerifyStep) that calls the verifier + this state machine.
1 parent 64c61b3 commit 8f58709

2 files changed

Lines changed: 213 additions & 4 deletions

File tree

crates/mosaic-chunked/src/lib.rs

Lines changed: 202 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
109134
impl 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);

crates/mosaic-core/src/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ pub enum OnChainError {
8787
SessionAlreadyInitialized = 0x0035,
8888
/// `CancelExpiredSession` called before `expires_at_slot`.
8989
SessionNotExpired = 0x0036,
90+
/// Chunked STARK verify `begin` step called before the session was
91+
/// finalized (the proof bytes are not yet complete).
92+
SessionNotFinalized = 0x0037,
93+
/// Chunked STARK verify `step` called before the `begin` setup step.
94+
StarkVerifyNotStarted = 0x0038,
95+
/// Chunked STARK verify `step` range does not contiguously advance
96+
/// the cursor (regressing, overshooting `num_queries`, or empty).
97+
StarkVerifyOutOfOrder = 0x0039,
9098

9199
// 0x0040..0x004F — syscall surface errors
92100
/// `sol_alt_bn128_group_op` returned a non-zero status.
@@ -146,6 +154,9 @@ impl OnChainError {
146154
Self::SessionContextMismatch => "session_context_mismatch",
147155
Self::SessionAlreadyInitialized => "session_already_initialized",
148156
Self::SessionNotExpired => "session_not_expired",
157+
Self::SessionNotFinalized => "session_not_finalized",
158+
Self::StarkVerifyNotStarted => "stark_verify_not_started",
159+
Self::StarkVerifyOutOfOrder => "stark_verify_out_of_order",
149160
Self::AltBn128SyscallFailed => "alt_bn128_syscall_failed",
150161
Self::AltBn128CompressionSyscallFailed => "alt_bn128_compression_syscall_failed",
151162
Self::PoseidonSyscallFailed => "poseidon_syscall_failed",

0 commit comments

Comments
 (0)