Skip to content

Commit e7761ed

Browse files
committed
Add gate check for trivial layers and fix single-proof API
1 parent 6ee8c77 commit e7761ed

4 files changed

Lines changed: 51 additions & 8 deletions

File tree

crypto/stark/src/gkr.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,10 +1085,18 @@ pub fn gkr_verify<E: IsField>(
10851085

10861086
if round_polys.is_empty() {
10871087
// Trivial layer (0 variables in parent): no sumcheck rounds.
1088-
// The prover just provides child_claims directly.
1089-
// No gate check here --- soundness is enforced by later layers' sumchecks.
1090-
// (The verifier's combined_claim may differ from the prover's by a
1091-
// scaling factor at the first trivial layer, so we skip the check.)
1088+
// Gate check: verify that n_claim * (dl·dr) = nl·dr + nr·dl.
1089+
//
1090+
// The verifier works in normalized form: n_claim = root_n/root_d, d_claim = 1.
1091+
// The prover's gate equation root_n + λ·root_d = nl·dr + nr·dl + λ·dl·dr,
1092+
// divided by root_d (= dl·dr), becomes n_claim·(dl·dr) = nl·dr + nr·dl
1093+
// (the λ terms cancel). This binds claimed_sum to the actual tree structure.
1094+
let [ref nl, ref nr, ref dl, ref dr] = layer_proof.child_claims;
1095+
let lhs = &n_claim * &(dl * dr);
1096+
let rhs = &(nl * dr) + &(nr * dl);
1097+
if lhs != rhs {
1098+
return Err(GkrError::GateCheckFailed { layer: layer_idx });
1099+
}
10921100
} else {
10931101
// Non-trivial layer: verify sumcheck inline.
10941102
let num_rounds = round_polys.len();
@@ -1985,7 +1993,33 @@ pub fn gkr_verify_batch<E: IsField>(
19851993
}
19861994

19871995
if round_polys.is_empty() {
1988-
// Trivial layer: no sumcheck needed
1996+
// Trivial layer: no sumcheck rounds.
1997+
// Gate check: verify that the alpha-batched combined claims match the
1998+
// alpha-batched gate evaluations (gate_i = nl·dr + nr·dl + λ·dl·dr, scaled
1999+
// by 2^n_unused_i to match combined_claims).
2000+
{
2001+
let mut actual_sum = FieldElement::<E>::zero();
2002+
let mut expected_sum = FieldElement::<E>::zero();
2003+
let mut alpha_pow = FieldElement::<E>::one();
2004+
for (idx, &i) in active_instances.iter().enumerate() {
2005+
let [ref nl, ref nr, ref dl, ref dr] =
2006+
layer_proof.child_claims_by_instance[idx];
2007+
let gate = &(&(nl * dr) + &(nr * dl)) + &(&lambda * &(dl * dr));
2008+
let n_unused = max_layers - n_layers_by_instance[i];
2009+
let gate_scaled = if n_unused > 0 {
2010+
&gate * &FieldElement::<E>::from(1u64 << n_unused)
2011+
} else {
2012+
gate
2013+
};
2014+
actual_sum = &actual_sum + &(&alpha_pow * &combined_claims[idx]);
2015+
expected_sum = &expected_sum + &(&alpha_pow * &gate_scaled);
2016+
alpha_pow = &alpha_pow * &sumcheck_alpha;
2017+
}
2018+
if actual_sum != expected_sum {
2019+
return Err(GkrError::GateCheckFailed { layer: layer_idx });
2020+
}
2021+
}
2022+
19892023
// Append child claims and sample eta (same as prover)
19902024
for claims in &layer_proof.child_claims_by_instance {
19912025
for claim in claims {

crypto/stark/src/proof/stark.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ pub struct StarkProof<F: IsSubFieldOf<E>, E: IsField, PI> {
8585
pub bus_public_inputs: Option<BusPublicInputs<E>>,
8686
// LogUp-GKR proof (when using GKR-based LogUp instead of per-row accumulated column)
8787
pub logup_gkr_proof: Option<LogUpGkrProof<E>>,
88+
// Batch GKR proof shared across all tables in a single-table prove/verify round-trip.
89+
// Populated by `Prover::prove` and consumed by `Verifier::verify`.
90+
#[serde(default)]
91+
pub batch_gkr_proof: Option<BatchGkrProof<E>>,
8892
// Public inputs used for boundary constraints
8993
pub public_inputs: PI,
9094
}

crypto/stark/src/prover.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2171,8 +2171,11 @@ pub trait IsStarkProver<
21712171
PI: Send + Sync + Clone,
21722172
{
21732173
let air_trace_pairs = vec![(air, trace, pub_inputs)];
2174-
Self::multi_prove(air_trace_pairs, transcript)
2175-
.map(|mut multi_proof| multi_proof.proofs.remove(0))
2174+
Self::multi_prove(air_trace_pairs, transcript).map(|mut multi_proof| {
2175+
let mut proof = multi_proof.proofs.remove(0);
2176+
proof.batch_gkr_proof = multi_proof.batch_gkr_proof;
2177+
proof
2178+
})
21762179
}
21772180

21782181
// TODO: propagate errors instead of unwrap() in open_deep_composition_poly and FRI operations
@@ -2335,6 +2338,8 @@ pub trait IsStarkProver<
23352338
bus_public_inputs: round_1_result.bus_public_inputs.clone(),
23362339
// LogUp-GKR proof (not yet used; will replace accumulated column in future)
23372340
logup_gkr_proof: None,
2341+
// Batch GKR proof: populated by Prover::prove after multi_prove completes.
2342+
batch_gkr_proof: None,
23382343
// Public inputs for boundary constraints
23392344
public_inputs: pub_inputs.clone(),
23402345
trace_length: domain.interpolation_domain_size,

crypto/stark/src/verifier.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ pub trait IsStarkVerifier<
10321032
{
10331033
let multi_proof = MultiProof {
10341034
proofs: vec![proof.clone()],
1035-
batch_gkr_proof: None,
1035+
batch_gkr_proof: proof.batch_gkr_proof.clone(),
10361036
};
10371037
Self::multi_verify(&[air], &multi_proof, transcript, &FieldElement::zero())
10381038
}

0 commit comments

Comments
 (0)