107107use crate :: {
108108 builder:: folder:: VerifierConstraintFolder ,
109109 ensure, ensure_eq,
110- lookup:: fingerprint ,
110+ lookup:: claims_accumulator ,
111111 prover:: Proof ,
112112 system:: System ,
113113 types:: { Challenger , ExtVal , FriParameters , Pcs , PcsError , StarkConfig , Val } ,
@@ -133,21 +133,44 @@ pub enum VerificationError<PcsErr> {
133133 InvalidProofShape ,
134134 /// The system configuration is invalid (e.g. no circuits).
135135 InvalidSystem ,
136+ /// The proof is not a full proof: either some claim has multiplicity ≠ 1
137+ /// or `remaining_claims` is non-empty. Use [`System::partially_verify`] instead.
138+ NotFullProof ,
136139 /// The recomputed composition polynomial does not match the quotient.
137140 OodEvaluationMismatch ,
138- /// The lookup accumulator did not balance to zero .
141+ /// The lookup accumulator did not reach the expected value .
139142 UnbalancedChannel ,
140143}
141144
142145impl < A : BaseAir < Val > + for < ' a > Air < VerifierConstraintFolder < ' a > > > System < A > {
143- /// Verifies a STARK proof. Claims are read from [`Proof:: claims`] .
146+ /// Verifies a complete STARK proof (all claim multiplicities 1, no remaining claims) .
144147 ///
145- /// Returns `Ok(())` if the proof is valid, or a [`VerificationError`] describing
146- /// the first check that failed .
148+ /// Checks that every claim has multiplicity 1 and that `remaining_claims` is empty,
149+ /// then delegates to [`Self::partially_verify`] .
147150 pub fn verify (
148151 & self ,
149152 fri_parameters : FriParameters ,
150153 proof : & Proof ,
154+ ) -> Result < ( ) , VerificationError < PcsError > > {
155+ ensure ! (
156+ proof. claims. iter( ) . all( |( _, m) | * m == 1 ) && proof. remaining_claims. is_empty( ) ,
157+ VerificationError :: NotFullProof
158+ ) ;
159+ self . partially_verify ( fri_parameters, proof)
160+ }
161+
162+ /// Verifies a STARK proof shard. Claims and remaining claims are read from the proof.
163+ ///
164+ /// The final lookup accumulator must equal the accumulator derived from
165+ /// `proof.remaining_claims`. The next shard's `claims` must equal these
166+ /// `remaining_claims` — it is the initial accumulator of the next shard (derived
167+ /// from its own claims) that must match, not its final accumulator.
168+ /// Use [`Self::verify`] for complete proofs where all multiplicities are 1 and
169+ /// `remaining_claims` is empty.
170+ pub fn partially_verify (
171+ & self ,
172+ fri_parameters : FriParameters ,
173+ proof : & Proof ,
151174 ) -> Result < ( ) , VerificationError < PcsError > > {
152175 let Proof {
153176 commitments,
@@ -159,21 +182,11 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
159182 stage_1_opened_values,
160183 stage_2_opened_values,
161184 claims,
185+ remaining_claims,
162186 } = proof;
163187 // first, verify the proof shape
164188 let quotient_degrees = self . verify_shape ( proof) ?;
165189
166- // Soundness: lookup argument. The accumulator was computed by the prover
167- // under challenges (β, γ) that were sampled after the traces and claims were
168- // committed. If the pushed and pulled multisets differ, the accumulator is a
169- // nonzero rational function of (β, γ) and evaluates to zero with probability
170- // ≤ N / |F_ext| (Schwartz-Zippel on the numerator polynomial).
171- ensure_eq ! (
172- intermediate_accumulators. last( ) ,
173- Some ( & ExtVal :: ZERO ) ,
174- VerificationError :: UnbalancedChannel
175- ) ;
176-
177190 // Soundness: Fiat-Shamir. All challenges below are derived deterministically
178191 // from the transcript via Keccak-256 (random oracle model). The verifier
179192 // replays exactly the same observations as the prover, so any divergence
@@ -194,11 +207,15 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
194207 challenger. observe ( Val :: from_u8 ( * log_degree) ) ;
195208 }
196209
197- // Soundness: claims must be observed BEFORE lookup challenges are sampled.
198- // Otherwise, the prover could choose claims adaptively after seeing the
199- // challenges, breaking the lookup argument's binding property.
200- for claim in claims {
201- challenger. observe_slice ( claim) ;
210+ // Soundness: claims and remaining_claims must be observed BEFORE lookup
211+ // challenges are sampled, otherwise the prover could choose them adaptively.
212+ for ( values, multiplicity) in claims {
213+ challenger. observe_slice ( values) ;
214+ challenger. observe ( Val :: from_u64 ( * multiplicity) ) ;
215+ }
216+ for ( values, multiplicity) in remaining_claims {
217+ challenger. observe_slice ( values) ;
218+ challenger. observe ( Val :: from_u64 ( * multiplicity) ) ;
202219 }
203220
204221 // Soundness: lookup argument. The challenges are random elements of F_ext.
@@ -213,13 +230,21 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
213230 // observe stage_2 commitment
214231 challenger. observe ( commitments. stage_2_trace . clone ( ) ) ;
215232
216- // construct the accumulator from the claims
217- let mut acc = ExtVal :: ZERO ;
218- for claim in claims {
219- let message = lookup_argument_challenge
220- + fingerprint ( & fingerprint_challenge, claim. iter ( ) . cloned ( ) ) ;
221- acc += message. inverse ( ) ;
222- }
233+ // initial accumulator from claims; target accumulator from remaining_claims
234+ let acc = claims_accumulator ( lookup_argument_challenge, fingerprint_challenge, claims) ;
235+ let remaining_acc = claims_accumulator (
236+ lookup_argument_challenge,
237+ fingerprint_challenge,
238+ remaining_claims,
239+ ) ;
240+
241+ // Soundness: the final intermediate accumulator must equal the remaining_acc.
242+ // With no remaining_claims, this is zero (fully balanced lookup argument).
243+ ensure_eq ! (
244+ intermediate_accumulators. last( ) ,
245+ Some ( & remaining_acc) ,
246+ VerificationError :: UnbalancedChannel
247+ ) ;
223248
224249 // Soundness: constraint folding. All k constraints are combined via powers
225250 // of α. The folded sum has degree k-1 in α, so by Schwartz-Zippel a violated
@@ -237,6 +262,7 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
237262 let mut stage_2_trace_evaluations = vec ! [ ] ;
238263 let mut quotient_chunks_evaluations = vec ! [ ] ;
239264 let mut last_quotient_i = 0 ;
265+ let mut curr_acc = acc;
240266 for i in 0 ..self . circuits . len ( ) {
241267 let log_degree = log_degrees[ i] ;
242268 let quotient_degree = quotient_degrees[ i] ;
@@ -365,7 +391,7 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
365391 let stage_2_public_values = & [
366392 lookup_argument_challenge,
367393 fingerprint_challenge,
368- acc ,
394+ curr_acc ,
369395 next_acc,
370396 ] ;
371397 let mut folder = VerifierConstraintFolder {
@@ -420,7 +446,7 @@ impl<A: BaseAir<Val> + for<'a> Air<VerifierConstraintFolder<'a>>> System<A> {
420446 VerificationError :: OodEvaluationMismatch
421447 ) ;
422448 // the accumulator must become the next accumulator for the next iteration
423- acc = next_acc;
449+ curr_acc = next_acc;
424450 }
425451
426452 Ok ( ( ) )
@@ -715,7 +741,7 @@ mod tests {
715741 let ( system, fri_parameters, mut proof) = small_system_and_proof ( ) ;
716742 let f = Val :: from_u32;
717743 // Tamper with the proof's claims — verifier transcript diverges from prover's.
718- proof. claims = vec ! [ vec![ f( 42 ) ] ] ;
744+ proof. claims = vec ! [ ( vec![ f( 42 ) ] , 1 ) ] ;
719745 let result = system. verify ( fri_parameters, & proof) ;
720746 assert ! ( result. is_err( ) ) ;
721747 }
@@ -755,4 +781,50 @@ mod tests {
755781 let proof2 = Proof :: from_bytes ( & bytes) . expect ( "deserialize" ) ;
756782 system. verify ( fri_parameters, & proof2) . unwrap ( ) ;
757783 }
784+
785+ #[ test]
786+ fn test_partial_proof_extra_claim_cancels ( ) {
787+ // A claim in remaining_claims sets the target accumulator for this shard.
788+ // The same claim in claims means this shard's initial accumulator already equals
789+ // that target, so with no circuit lookups the final accumulator matches and
790+ // partially_verify accepts. This simulates passing the claim to the next shard.
791+ let commitment_parameters = CommitmentParameters {
792+ log_blowup : 1 ,
793+ cap_height : 0 ,
794+ } ;
795+ let ( system, key) = system ( commitment_parameters) ;
796+ let f = Val :: from_u32;
797+ let witness = SystemWitness :: from_stage_1 (
798+ vec ! [
799+ RowMajorMatrix :: new(
800+ [ 3 , 4 , 5 , 5 , 12 , 13 , 8 , 15 , 17 , 7 , 24 , 25 ] . map( f) . to_vec( ) ,
801+ 3 ,
802+ ) ,
803+ RowMajorMatrix :: new( [ 4 , 2 , 3 , 1 , 10 , 10 , 3 , 2 , 5 , 1 , 13 , 13 ] . map( f) . to_vec( ) , 6 ) ,
804+ ] ,
805+ & system,
806+ ) ;
807+ let fri_parameters = FriParameters {
808+ log_final_poly_len : 0 ,
809+ max_log_arity : 1 ,
810+ num_queries : 64 ,
811+ commit_proof_of_work_bits : 0 ,
812+ query_proof_of_work_bits : 0 ,
813+ } ;
814+ let extra_claim = vec ! [ f( 42 ) , f( 43 ) ] ;
815+ let proof = system. partially_prove (
816+ fri_parameters,
817+ & key,
818+ vec ! [ ( extra_claim. clone( ) , 1 ) ] ,
819+ vec ! [ ( extra_claim, 1 ) ] ,
820+ witness,
821+ ) ;
822+ // partially_verify must accept — claim and remaining cancel
823+ system. partially_verify ( fri_parameters, & proof) . unwrap ( ) ;
824+ // verify must reject — remaining_claims is non-empty
825+ assert ! ( matches!(
826+ system. verify( fri_parameters, & proof) ,
827+ Err ( VerificationError :: NotFullProof )
828+ ) ) ;
829+ }
758830}
0 commit comments