Skip to content

Commit efa99f9

Browse files
Partial proofs
1 parent 10aba15 commit efa99f9

3 files changed

Lines changed: 164 additions & 56 deletions

File tree

src/lookup.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use p3_air::{Air, BaseAir, ExtensionBuilder, WindowAccess};
2-
use p3_field::{PrimeCharacteristicRing, batch_multiplicative_inverse};
2+
use p3_field::{Field, PrimeCharacteristicRing, batch_multiplicative_inverse};
33
use p3_matrix::{Matrix, dense::RowMajorMatrix};
44
use p3_maybe_rayon::prelude::*;
55

@@ -74,6 +74,24 @@ impl<A: BaseAir<Val>> LookupAir<A> {
7474
}
7575
}
7676

77+
/// Computes the accumulator contribution from a list of claims with multiplicities.
78+
///
79+
/// Both the prover and verifier use this to initialize the lookup accumulator from
80+
/// external claims before running the circuit-level lookup argument.
81+
pub(crate) fn claims_accumulator(
82+
lookup_argument_challenge: ExtVal,
83+
fingerprint_challenge: ExtVal,
84+
claims: &[(Vec<Val>, u64)],
85+
) -> ExtVal {
86+
let mut acc = ExtVal::ZERO;
87+
for (claim, multiplicity) in claims {
88+
let message =
89+
lookup_argument_challenge + fingerprint(&fingerprint_challenge, claim.iter().cloned());
90+
acc += message.inverse() * ExtVal::from_u64(*multiplicity);
91+
}
92+
acc
93+
}
94+
7795
/// Computes a fingerprint of the coefficients using Horner's method.
7896
#[inline]
7997
pub(crate) fn fingerprint<F, I, Iter>(r: &F, coeffs: Iter) -> F

src/prover.rs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ use std::ops::Deref;
154154

155155
use crate::{
156156
builder::folder::ProverConstraintFolder,
157-
lookup::{Lookup, fingerprint},
157+
lookup::{Lookup, claims_accumulator},
158158
system::{ProverKey, System, SystemWitness},
159159
types::{
160160
Challenger, Commitment, Domain, EvaluationsOnDomain, ExtVal, FriParameters, PackedExtVal,
@@ -170,8 +170,7 @@ use p3_air::{Air, BaseAir, RowWindow};
170170
use p3_challenger::{CanObserve, FieldChallenger};
171171
use p3_commit::{LagrangeSelectors, OpenedValuesForRound, Pcs as PcsTrait, PolynomialSpace};
172172
use p3_field::{
173-
BasedVectorSpace, Field, PackedValue, PrimeCharacteristicRing,
174-
extension::BinomialExtensionField,
173+
BasedVectorSpace, PackedValue, PrimeCharacteristicRing, extension::BinomialExtensionField,
175174
};
176175
use p3_matrix::{Matrix, dense::RowMajorMatrix};
177176
use p3_maybe_rayon::prelude::*;
@@ -203,9 +202,13 @@ pub struct Proof {
203202
pub preprocessed_opened_values: Option<OpenedValuesForRound<ExtVal>>,
204203
pub stage_1_opened_values: OpenedValuesForRound<ExtVal>,
205204
pub stage_2_opened_values: OpenedValuesForRound<ExtVal>,
206-
/// Claims bound to this proof, observed into the Fiat-Shamir transcript before
207-
/// lookup challenges are sampled. The verifier reads claims directly from here.
208-
pub claims: Vec<Vec<Val>>,
205+
/// Claims with multiplicities bound to this proof. Observed into the Fiat-Shamir
206+
/// transcript before lookup challenges are sampled. The verifier reads them directly.
207+
pub claims: Vec<(Vec<Val>, u64)>,
208+
/// Remaining claims with multiplicities that must become the `claims` of the next
209+
/// proof shard. The final lookup accumulator equals the accumulator derived from these,
210+
/// and the next shard's initial accumulator (from its own claims) must match.
211+
pub remaining_claims: Vec<(Vec<Val>, u64)>,
209212
}
210213

211214
impl Proof {
@@ -226,16 +229,31 @@ impl Proof {
226229
}
227230

228231
impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
229-
/// Generates a STARK proof for the system.
230-
///
231-
/// Each claim is a vector of field elements observed by the challenger before
232-
/// lookup challenges are sampled, binding the proof to the claimed values.
232+
/// Generates a full STARK proof binding the given claims (all with multiplicity 1).
233233
pub fn prove(
234234
&self,
235235
fri_parameters: FriParameters,
236236
key: &ProverKey,
237237
claims: Vec<Vec<Val>>,
238238
witness: SystemWitness,
239+
) -> Proof {
240+
let claims = claims.into_iter().map(|c| (c, 1u64)).collect();
241+
self.partially_prove(fri_parameters, key, claims, vec![], witness)
242+
}
243+
244+
/// Generates a partial STARK proof (proof shard).
245+
///
246+
/// `claims` with multiplicities are observed into the Fiat-Shamir transcript before
247+
/// lookup challenges are sampled. `remaining_claims` encode the obligations passed to
248+
/// the next shard: the next shard's `claims` must equal these (so their accumulators
249+
/// match), and this shard's final accumulator equals the accumulator derived from them.
250+
pub fn partially_prove(
251+
&self,
252+
fri_parameters: FriParameters,
253+
key: &ProverKey,
254+
claims: Vec<(Vec<Val>, u64)>,
255+
remaining_claims: Vec<(Vec<Val>, u64)>,
256+
witness: SystemWitness,
239257
) -> Proof {
240258
// initialize pcs and challenger
241259
let config = StarkConfig::new(self.commitment_parameters, fri_parameters);
@@ -266,11 +284,15 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
266284
challenger.observe(Val::from_usize(*log_degree));
267285
}
268286

269-
// observe the claims
270-
// this has to be done before generating the lookup argument challenge
271-
// otherwise the lookup argument can be attacked
272-
for claim in &claims {
273-
challenger.observe_slice(claim);
287+
// observe claims and remaining claims before lookup challenges are sampled;
288+
// this is required for soundness of the lookup argument
289+
for (values, multiplicity) in &claims {
290+
challenger.observe_slice(values);
291+
challenger.observe(Val::from_u64(*multiplicity));
292+
}
293+
for (values, multiplicity) in &remaining_claims {
294+
challenger.observe_slice(values);
295+
challenger.observe(Val::from_u64(*multiplicity));
274296
}
275297

276298
// generate lookup challenges
@@ -279,13 +301,7 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
279301
let fingerprint_challenge: ExtVal = challenger.sample_algebra_element();
280302
challenger.observe_algebra_element(fingerprint_challenge);
281303

282-
// construct the accumulator from the claims
283-
let mut acc = ExtVal::ZERO;
284-
for claim in &claims {
285-
let message = lookup_argument_challenge
286-
+ fingerprint(&fingerprint_challenge, claim.iter().cloned());
287-
acc += message.inverse();
288-
}
304+
let acc = claims_accumulator(lookup_argument_challenge, fingerprint_challenge, &claims);
289305

290306
// Cost: "Lookup trace construction" — fingerprint (Horner), batch
291307
// inversion, and accumulator update. Total: Σ n_i·L_i extension field ops.
@@ -316,6 +332,7 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
316332
debug_assert_eq!(intermediate_accumulators.len(), self.circuits.len());
317333
debug_assert_eq!(log_degrees.len(), self.circuits.len());
318334
let mut quotient_degrees = vec![];
335+
let mut curr_acc = acc;
319336
let quotient_evaluations = self
320337
.circuits
321338
.iter()
@@ -365,7 +382,7 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
365382
let stage_2_public_values = [
366383
lookup_argument_challenge,
367384
fingerprint_challenge,
368-
acc,
385+
curr_acc,
369386
*next_acc,
370387
];
371388
let quotient_values = quotient_values(
@@ -389,7 +406,7 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
389406
let quotient_sub_domains = quotient_domain.split_domains(quotient_degree);
390407
// need to save the quotient degree for later
391408
quotient_degrees.push(quotient_degree);
392-
acc = *next_acc;
409+
curr_acc = *next_acc;
393410
quotient_sub_domains
394411
.into_iter()
395412
.zip(quotient_sub_evaluations)
@@ -456,6 +473,7 @@ impl<A: BaseAir<Val> + for<'a> Air<ProverConstraintFolder<'a>>> System<A> {
456473
stage_1_opened_values,
457474
stage_2_opened_values,
458475
claims,
476+
remaining_claims,
459477
}
460478
}
461479
}

src/verifier.rs

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
use 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

142145
impl<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

Comments
 (0)