Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion crypto/stark/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,13 @@ where
.map(|c| c.degree())
.max()
.unwrap_or(1);
trace_length * max_degree
// The composition polynomial is the constraint QUOTIENT H = Σ βᵢ·Cᵢ/Zᵢ. Its degree is
// deg(Cᵢ) − deg(Zᵢ) = (max_degree−1)·N − max_degree + eᵢ, so with the end-exemptions
// eᵢ < max_degree (the max-degree LogUp constraints have eᵢ = 0) it fits in
// (max_degree−1) parts — the max_degree-th part is identically zero. The tight bound is
// therefore (max_degree−1)·N; the previous max_degree·N committed and opened a wasted
// all-zero part (e.g. 3 parts for a degree-3 AIR where 2 suffice).
trace_length * (max_degree - 1).max(1)
}

fn context(&self) -> &AirContext {
Expand Down
3 changes: 2 additions & 1 deletion crypto/stark/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,8 @@ pub trait IsStarkProver<
// Compute entirely in base field — mixed F×E multiplication when used with extension values.
let two_base = FieldElement::<Field>::from(2u64);
let mut inv_2x: Vec<FieldElement<Field>> = (0..n)
.map(|i| &two_base * &domain.lde_roots_of_unity_coset[i])
// 2·(g·ωⁱ) = (g·ωⁱ).double() — one add, vs a base mul+reduce per element.
.map(|i| domain.lde_roots_of_unity_coset[i].double())
.collect();
FieldElement::inplace_batch_inverse(&mut inv_2x).expect("Coset points are non-zero");

Expand Down
55 changes: 55 additions & 0 deletions crypto/stark/src/tests/bus_tests/soundness_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,61 @@ fn test_wrong_result_value() {
));
}

/// The composition-poly part count is fixed by the AIR's max constraint degree,
/// not chosen by the prover. A proof advertising a different number of parts must
/// be rejected — otherwise a malicious prover could inflate the parts to widen the
/// composition polynomial's degree space and weaken the low-degree test.
#[test_log::test]
fn test_rejects_inflated_composition_part_count() {
// All-padding traces: a valid, bus-balanced (Σ = 0) proof — the simplest valid case.
let mut cpu_trace = TraceTable::from_columns_main(vec![vec![FE::zero(); 4]; 5], 1);
let mut add_trace = TraceTable::from_columns_main(vec![vec![FE::zero(); 4]; 4], 1);
let mut mul_trace = TraceTable::from_columns_main(vec![vec![FE::zero(); 4]; 4], 1);

let proof_options = ProofOptions::default_test_options();
let cpu_air = new_cpu_air_with_lookup(&proof_options);
let add_air = new_add_air_with_lookup(&proof_options);
let mul_air = new_mul_air_with_lookup(&proof_options);

let air_trace_pairs: Vec<(
&dyn AIR<Field = F, FieldExtension = E, PublicInputs = ()>,
_,
_,
)> = vec![
(&cpu_air, &mut cpu_trace, &()),
(&add_air, &mut add_trace, &()),
(&mul_air, &mut mul_trace, &()),
];
let mut multi_proof =
multi_prove_ram(air_trace_pairs, &mut DefaultTranscript::<E>::new(&[])).unwrap();

let airs: Vec<&dyn AIR<Field = F, FieldExtension = E, PublicInputs = ()>> =
vec![&cpu_air, &add_air, &mul_air];

// The untampered proof verifies.
assert!(Verifier::multi_verify(
&airs,
&multi_proof,
&mut DefaultTranscript::<E>::new(&[]),
&FieldElement::zero(),
));

// Tamper: inflate the first table's composition-poly part count.
multi_proof.proofs[0]
.composition_poly_parts_ood_evaluation
.push(FieldElement::<E>::zero());

assert!(
!Verifier::multi_verify(
&airs,
&multi_proof,
&mut DefaultTranscript::<E>::new(&[]),
&FieldElement::zero(),
),
"verifier must reject a composition part count that disagrees with the AIR degree bound"
);
}

/// Off-by-one error: CPU sends (5, 3, 8) but ADD claims (5, 3, 9).
#[test_log::test]
fn test_off_by_one() {
Expand Down
11 changes: 11 additions & 0 deletions crypto/stark/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,17 @@ pub trait IsStarkVerifier<
// trust the prover). For normal tables, use the commitment from the proof.

for (idx, (air, proof)) in airs.iter().zip(&multi_proof.proofs).enumerate() {
// Soundness: the number of composition-poly parts is fixed by the AIR's
// degree bound, NOT chosen by the prover. Deriving it from the proof would
// let a malicious prover inflate the part count, widening the composition
// polynomial's degree space and weakening the low-degree test. Reject any
// proof whose advertised part count disagrees with the AIR.
if proof.trace_length == 0
|| proof.composition_poly_parts_ood_evaluation.len()
!= air.composition_poly_degree_bound(proof.trace_length) / proof.trace_length
{
return false;
}
if air.is_preprocessed() {
// Preprocessed table: VERIFY precomputed commitment matches hardcoded.
// This is the critical soundness check - ensures prover used correct precomputed values.
Expand Down
Loading