Skip to main content

binius_spartan_verifier/
constraint_system.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use binius_field::Field;
5use binius_ip::mlecheck::mask_buffer_dimensions;
6pub use binius_spartan_frontend::constraint_system::BlindingInfo;
7use binius_spartan_frontend::constraint_system::{
8	ConstraintSystem, MulConstraint, Operand, Witness, WitnessIndex,
9};
10use binius_utils::checked_arithmetics::{checked_log_2, log2_ceil_usize};
11
12/// A constraint system with blinding and power-of-two padding.
13///
14/// Wraps a [`ConstraintSystem`], adds dummy constraints for blinding, and pads the total
15/// number of constraints to a power of two (required by the prover's multilinear extension
16/// protocol).
17#[derive(Debug, Clone)]
18pub struct ConstraintSystemPadded<F: Field> {
19	inner: ConstraintSystem<F>,
20	log_precommit: u32,
21	log_private: u32,
22	blinding_info: BlindingInfo,
23	mul_constraints: Vec<MulConstraint<WitnessIndex>>,
24	/// Mask buffer dimensions (m_n, m_d) for the ZK mulcheck mask polynomial.
25	mask_dims: (usize, usize),
26}
27
28impl<F: Field> ConstraintSystemPadded<F> {
29	/// Create a new padded constraint system with blinding.
30	///
31	/// This:
32	/// 1. Adds dummy multiplication constraints for blinding (3 wires each: A * B = C)
33	/// 2. Pads the total constraint count to a power of two with `one * one = one` constraints
34	/// 3. Calculates the log_size based on witness requirements
35	/// 4. Computes mask buffer dimensions for the ZK mulcheck mask polynomial
36	pub fn new(cs: ConstraintSystem<F>, blinding_info: BlindingInfo) -> Self {
37		let mut mul_constraints = cs.mul_constraints().to_vec();
38
39		/// Adds dummy blinding constraints for a segment and returns its padded log-size.
40		fn add_blinding_constraints(
41			mul_constraints: &mut Vec<MulConstraint<WitnessIndex>>,
42			make_index: fn(u32) -> WitnessIndex,
43			n_circuit_wires: usize,
44			n_dummy_wires: usize,
45			n_dummy_constraints: usize,
46		) -> u32 {
47			let dummy_base = n_circuit_wires + n_dummy_wires;
48			for i in 0..n_dummy_constraints {
49				let a = make_index((dummy_base + 3 * i) as u32);
50				let b = make_index((dummy_base + 3 * i + 1) as u32);
51				let c = make_index((dummy_base + 3 * i + 2) as u32);
52				mul_constraints.push(MulConstraint {
53					a: Operand::from(a),
54					b: Operand::from(b),
55					c: Operand::from(c),
56				});
57			}
58
59			let blinding_size = n_dummy_wires + 3 * n_dummy_constraints;
60			log2_ceil_usize(n_circuit_wires + blinding_size) as u32
61		}
62
63		// Both committed segments have evaluations revealed in the clear, so both need dummy
64		// constraints to carry randomness into the wiring relation that masks them.
65		let log_precommit = add_blinding_constraints(
66			&mut mul_constraints,
67			WitnessIndex::precommit,
68			cs.n_precommit() as usize,
69			blinding_info.n_dummy_wires,
70			blinding_info.n_dummy_constraints,
71		);
72		let log_private = add_blinding_constraints(
73			&mut mul_constraints,
74			WitnessIndex::private,
75			cs.n_private() as usize,
76			blinding_info.n_dummy_wires,
77			blinding_info.n_dummy_constraints,
78		);
79
80		// Pad to next power of two with `one * one = one` constraints
81		let one_operand = Operand::from(cs.one_wire());
82		let current_len = mul_constraints.len();
83		mul_constraints.resize(
84			current_len.next_power_of_two(),
85			MulConstraint {
86				a: one_operand.clone(),
87				b: one_operand.clone(),
88				c: one_operand,
89			},
90		);
91
92		// Calculate mask buffer dimensions
93		let log_mul_constraints = checked_log_2(mul_constraints.len());
94		let mask_degree = 2; // quadratic composition
95		let mask_dims =
96			mask_buffer_dimensions(log_mul_constraints, mask_degree, blinding_info.n_dummy_wires);
97
98		Self {
99			inner: cs,
100			log_precommit,
101			log_private,
102			blinding_info,
103			mul_constraints,
104			mask_dims,
105		}
106	}
107
108	pub fn constants(&self) -> &[F] {
109		self.inner.constants()
110	}
111
112	pub const fn n_inout(&self) -> u32 {
113		self.inner.n_inout()
114	}
115
116	pub const fn n_precommit(&self) -> u32 {
117		self.inner.n_precommit()
118	}
119
120	pub const fn n_private(&self) -> u32 {
121		self.inner.n_private()
122	}
123
124	pub const fn log_public(&self) -> u32 {
125		self.inner.log_public()
126	}
127
128	pub const fn n_public(&self) -> u32 {
129		self.inner.n_public()
130	}
131
132	pub const fn one_wire(&self) -> WitnessIndex {
133		self.inner.one_wire()
134	}
135
136	pub const fn log_precommit(&self) -> u32 {
137		self.log_precommit
138	}
139
140	pub const fn precommit_size(&self) -> usize {
141		1 << self.log_precommit as usize
142	}
143
144	pub const fn log_private(&self) -> u32 {
145		self.log_private
146	}
147
148	pub const fn private_size(&self) -> usize {
149		1 << self.log_private as usize
150	}
151
152	pub const fn blinding_info(&self) -> &BlindingInfo {
153		&self.blinding_info
154	}
155
156	pub fn mul_constraints(&self) -> &[MulConstraint<WitnessIndex>] {
157		&self.mul_constraints
158	}
159
160	/// Returns the mask buffer dimensions (m_n, m_d) for the ZK mulcheck mask polynomial.
161	pub const fn mask_dims(&self) -> (usize, usize) {
162		self.mask_dims
163	}
164
165	pub fn validate(&self, witness: &Witness<F>) {
166		assert_eq!(witness.public().len(), 1 << self.log_public() as usize);
167		assert_eq!(witness.private().len(), self.private_size());
168
169		let operand_val = |operand: &Operand<WitnessIndex>| {
170			operand.wires().iter().map(|&idx| witness[idx]).sum::<F>()
171		};
172
173		for MulConstraint { a, b, c } in &self.mul_constraints {
174			assert_eq!(operand_val(a) * operand_val(b), operand_val(c));
175		}
176	}
177}
178
179#[cfg(test)]
180mod tests {
181	use std::collections::BTreeSet;
182
183	use binius_field::Ghash128b as B128;
184	use binius_spartan_frontend::{
185		circuit_builder::{CircuitBuilder, ConstraintBuilder},
186		compiler::compile,
187		constraint_system::WitnessSegment,
188	};
189
190	use super::*;
191
192	#[test]
193	fn every_committed_segment_reserves_one_wire_beyond_the_fri_queries() {
194		// Each FRI query opens one Merkle leaf, revealing one codeword symbol of the segment.
195		const N_TEST_QUERIES: usize = 32;
196
197		// Any circuit will do: blinding is padding appended after whatever real wires exist.
198		let mut builder = ConstraintBuilder::<B128>::new();
199		let x = builder.alloc_inout();
200		let y = builder.alloc_inout();
201		builder.assert_eq(x, y);
202		let (cs, _layout) = compile(builder);
203
204		let n_precommit = cs.n_precommit() as usize;
205		let n_private = cs.n_private() as usize;
206
207		let info = BlindingInfo::for_fri_queries(N_TEST_QUERIES);
208		let padded = ConstraintSystemPadded::new(cs, info);
209
210		// Invariant: the dummy wires must outnumber the queries.
211		// Spending exactly one per query would leave the unopened leaves with no randomness of
212		// their own, and the leaves carry no salt.
213		assert!(info.n_dummy_wires > N_TEST_QUERIES);
214
215		// Each segment is rounded up to a power of two, so its reserved size must still cover
216		// every real wire plus the whole blinding allowance, which is the same for both:
217		//
218		//     n_dummy_wires + 3 * n_dummy_constraints
219		let blinding = info.n_dummy_wires + 3 * info.n_dummy_constraints;
220		assert!(padded.precommit_size() >= n_precommit + blinding);
221		assert!(padded.private_size() >= n_private + blinding);
222	}
223
224	#[test]
225	fn every_committed_segment_masks_its_revealed_evaluations() {
226		// A revealed evaluation weights each wire by its coefficient in the wiring relation.
227		//
228		// That relation only ever sums over wires that appear in a multiplication constraint, so
229		// a wire in no constraint is weighted by zero and can mask nothing.
230		//
231		// This pins where each segment's blinding lands:
232		//
233		//     dummy wires        in no constraint -> mask the codeword symbols FRI opens
234		//     dummy constraints  in a constraint -> mask the evaluations sent in the clear
235		const N_TEST_QUERIES: usize = 8;
236
237		let mut builder = ConstraintBuilder::<B128>::new();
238		let x = builder.alloc_inout();
239		let y = builder.alloc_inout();
240		builder.assert_eq(x, y);
241		let (cs, _layout) = compile(builder);
242
243		let n_circuit = [cs.n_precommit() as usize, cs.n_private() as usize];
244		let info = BlindingInfo::for_fri_queries(N_TEST_QUERIES);
245		let padded = ConstraintSystemPadded::new(cs, info);
246
247		// Collect, per segment, the wire indices the constraints actually touch.
248		let mut in_support = [BTreeSet::new(), BTreeSet::new()];
249		for constraint in padded.mul_constraints() {
250			for operand in [&constraint.a, &constraint.b, &constraint.c] {
251				for wire in operand.wires() {
252					let slot = match wire.segment {
253						WitnessSegment::Precommit => 0,
254						WitnessSegment::Private => 1,
255						WitnessSegment::Public => continue,
256					};
257					in_support[slot].insert(wire.index as usize);
258				}
259			}
260		}
261
262		for (segment, n_circuit) in in_support.iter().zip(n_circuit) {
263			// The dummy wires sit immediately after the circuit's own wires, and none of them may
264			// appear in a constraint — that is what makes them useless against an evaluation.
265			for offset in 0..info.n_dummy_wires {
266				assert!(
267					!segment.contains(&(n_circuit + offset)),
268					"a dummy wire reached the wiring relation"
269				);
270			}
271
272			// The dummy constraints follow, and all three of their wires must appear, or the
273			// evaluations revealed for this segment have nothing masking them.
274			let dummy_constraint_base = n_circuit + info.n_dummy_wires;
275			for offset in 0..3 * info.n_dummy_constraints {
276				assert!(
277					segment.contains(&(dummy_constraint_base + offset)),
278					"a dummy constraint wire never reached the wiring relation"
279				);
280			}
281		}
282	}
283}