From 5bb336dab71673c6f34f11fb29ab46a1f1f5730d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Mon, 6 Jul 2026 16:36:28 -0300 Subject: [PATCH] fix(prover): bind fri_final_poly_log_degree into the continuation-global statement #729 binds `fri_final_poly_log_degree` into the monolithic statement (STATEMENT_V3) and the continuation-epoch statement (CONTINUATION_EPOCH_V2), but not into `absorb_continuation_global_statement`. The cross-epoch global proof is itself a STARK produced and verified under the same ProofOptions, so its FRI transcript shape depends on `k` just like the others; leaving it unbound makes the canonical-binding guarantee half-applied and contradicts the global statement's own doc comment ("canonically pinned, like the monolithic path's absorb_statement"). Absorb the byte and bump CONTINUATION_GLOBAL_V1 -> V2. A mismatch could only ever reject (the verifier derives every FRI parameter from its own options and structurally checks the proof), so this is defense-in-depth/consistency, not a soundness fix. Adds the `must bind fri_final_poly_log_degree` assertion to the global-statement test; continuation prove/verify roundtrips still pass. --- prover/src/continuation.rs | 11 ++++++++++- prover/src/statement.rs | 10 ++++++++-- prover/src/tests/statement_tests.rs | 21 ++++++++++++++------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 105ac92a8..c5c2fc944 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -110,6 +110,7 @@ fn global_transcript( elf_bytes: &[u8], num_epochs: usize, num_private_input_pages: usize, + fri_final_poly_log_degree: u8, touched_page_bases: &[u64], ) -> DefaultTranscript { let mut transcript = DefaultTranscript::::new(&[]); @@ -118,6 +119,7 @@ fn global_transcript( elf_bytes, num_epochs, num_private_input_pages, + fri_final_poly_log_degree, touched_page_bases, ); transcript @@ -686,6 +688,7 @@ fn prove_global( elf_bytes, boundaries.len(), num_private_input_pages, + opts.fri_final_poly_log_degree, page_bases, ), #[cfg(feature = "disk-spill")] @@ -730,7 +733,13 @@ fn verify_global( Verifier::multi_verify( &refs, proof, - &mut global_transcript(elf_bytes, num_epochs, num_private_input_pages, page_bases), + &mut global_transcript( + elf_bytes, + num_epochs, + num_private_input_pages, + opts.fri_final_poly_log_degree, + page_bases, + ), &FieldElement::zero(), ) } diff --git a/prover/src/statement.rs b/prover/src/statement.rs index e2ca27fef..87dab84cd 100644 --- a/prover/src/statement.rs +++ b/prover/src/statement.rs @@ -125,20 +125,23 @@ pub(crate) fn absorb_statement( /// Continuation domain tags. Distinct from the monolithic `DOMAIN_TAG` so a /// monolithic proof and a continuation proof can never share a transcript prefix. const CONTINUATION_EPOCH_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_EPOCH_V2"; -const CONTINUATION_GLOBAL_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_GLOBAL_V1"; +const CONTINUATION_GLOBAL_TAG: &[u8] = b"LAMBDAVM_CONTINUATION_GLOBAL_V2"; /// Statement bound into the cross-epoch **global** proof's transcript before /// Phase A: the ELF (so the global proof is program-bound), the epoch count (so a /// global proof from a run with a different number of epochs cannot be spliced in), /// the private-input page count (so the global proof's AIR layout — which touched pages /// are built non-preprocessed — is canonically pinned, like the monolithic path's -/// `absorb_statement`), and the touched page-base set (which GLOBAL_MEMORY tables exist). +/// `absorb_statement`), `fri_final_poly_log_degree` (which sets the FRI transcript +/// shape, exactly as the monolithic and epoch statements bind it), and the touched +/// page-base set (which GLOBAL_MEMORY tables exist). /// Prove and verify must call this with identical arguments. pub(crate) fn absorb_continuation_global_statement( t: &mut impl IsTranscript, elf_bytes: &[u8], num_epochs: usize, num_private_input_pages: usize, + fri_final_poly_log_degree: u8, touched_page_bases: &[u64], ) { t.append_bytes(CONTINUATION_GLOBAL_TAG); @@ -146,6 +149,9 @@ pub(crate) fn absorb_continuation_global_statement( t.append_bytes(&(num_epochs as u64).to_le_bytes()); t.append_bytes(&(num_private_input_pages as u64).to_le_bytes()); + // fri_final_poly_log_degree: single byte, no endianness concern. + t.append_bytes(&[fri_final_poly_log_degree]); + // Touched page-base set: count-prefixed, each fixed-width u64. Binds the exact set // (and order) of GLOBAL_MEMORY tables the verifier rebuilds, so a tampered list // diverges the challenges. Prover and verifier pass the identical canonical diff --git a/prover/src/tests/statement_tests.rs b/prover/src/tests/statement_tests.rs index 75f8fb8e3..d3dafc0c7 100644 --- a/prover/src/tests/statement_tests.rs +++ b/prover/src/tests/statement_tests.rs @@ -178,6 +178,7 @@ fn global_state( elf: &[u8], num_epochs: usize, num_private_input_pages: usize, + fri_final_poly_log_degree: u8, touched_page_bases: &[u64], ) -> [u8; 32] { let mut t = DefaultTranscript::::new(&[]); @@ -186,6 +187,7 @@ fn global_state( elf, num_epochs, num_private_input_pages, + fri_final_poly_log_degree, touched_page_bases, ); t.state() @@ -193,31 +195,36 @@ fn global_state( #[test] fn continuation_global_state_binds_program_epoch_count_pages_and_touched_set() { - let baseline = global_state(b"elf", 3, 1, &[0x1000, 0x2000]); - assert_eq!(baseline, global_state(b"elf", 3, 1, &[0x1000, 0x2000])); // deterministic + let baseline = global_state(b"elf", 3, 1, 7, &[0x1000, 0x2000]); + assert_eq!(baseline, global_state(b"elf", 3, 1, 7, &[0x1000, 0x2000])); // deterministic assert_ne!( baseline, - global_state(b"elf", 4, 1, &[0x1000, 0x2000]), + global_state(b"elf", 4, 1, 7, &[0x1000, 0x2000]), "must bind epoch count" ); assert_ne!( baseline, - global_state(b"other-elf", 3, 1, &[0x1000, 0x2000]), + global_state(b"other-elf", 3, 1, 7, &[0x1000, 0x2000]), "must bind the ELF" ); assert_ne!( baseline, - global_state(b"elf", 3, 2, &[0x1000, 0x2000]), + global_state(b"elf", 3, 2, 7, &[0x1000, 0x2000]), "must bind the private-input page count" ); assert_ne!( baseline, - global_state(b"elf", 3, 1, &[0x1000, 0x3000]), + global_state(b"elf", 3, 1, 8, &[0x1000, 0x2000]), + "must bind fri_final_poly_log_degree" + ); + assert_ne!( + baseline, + global_state(b"elf", 3, 1, 7, &[0x1000, 0x3000]), "must bind the touched page-base set" ); assert_ne!( baseline, - global_state(b"elf", 3, 1, &[0x1000]), + global_state(b"elf", 3, 1, 7, &[0x1000]), "must bind the touched page-base count" ); }