Skip to main content

binius_core/constraint_system/
system.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::iter;
4
5use binius_utils::{
6	checked_arithmetics::log2_ceil_usize,
7	serialization::{DeserializeBytes, SerializationError, SerializeBytes},
8};
9use bytes::{Buf, BufMut};
10
11use super::{
12	AndConstraint, BmulConstraint, Composition, ConstraintKind, ImulConstraint, Operand, Shift,
13	ValueIndex, ValueSegment, ValueVec, ZeroConstraint,
14};
15use crate::{
16	error::{ConstraintSystemError, OperandFault, VerificationError},
17	word::Word,
18};
19
20/// Which of the two value-vector segments holds the inout values.
21///
22/// The constants are always public and the private values always hidden, so this is the only
23/// freedom in where the segment boundary falls. A proving protocol picks the placement that suits
24/// how its verifier learns the inout words, and passes it to every accessor that reports a segment
25/// length.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum InoutSegment {
28	/// The inout values are public: the verifier knows every one of them, so the reduction reads
29	/// them as shared data and nothing about them is committed.
30	Public,
31	/// The inout values are hidden: they are committed with the private values.
32	///
33	/// This is what a data-parallel protocol needs. Each instance chooses its own inout words, so
34	/// they are not one set of shared values the verifier can evaluate.
35	Hidden,
36}
37
38/// The ConstraintSystem is the core data structure in Binius64 that defines the computational
39/// constraints to be proven in zero-knowledge. It represents a system of equations over 64-bit
40/// words that must be satisfied by a valid values vector [`ValueVec`].
41///
42/// # Value vector shape
43///
44/// The constraints reference words of a value vector partitioned into two segments. The public
45/// segment holds the words the verifier evaluates itself; the hidden segment holds the words the
46/// prover commits. The constants are always public and the private values always hidden; the inout
47/// values sit in whichever segment the proving protocol places them in, which every segment-length
48/// accessor takes as an [`InoutSegment`]. Each group of values is followed by padding, so under
49/// [`InoutSegment::Public`] the value vector runs
50///
51/// ```text
52/// [ constants | inout | pad ][ private | pad ]
53///  \--- public segment ---/  \- hidden segment -/
54/// ```
55///
56/// Both segments are padded by the proving protocol rather than by the system: the public segment
57/// up to a power of two, and the hidden segment up to at least the public length. The system
58/// stores the value counts and derives the padded lengths from them.
59///
60/// A constraint names a word by its [`ValueSegment`] and its position within that segment, so
61/// the padding is unaddressable: an index reaches only the values of its own segment, and where
62/// those values sit in the vector is the layout's business rather than the constraint's.
63///
64/// # Constraint counts
65///
66/// The ZERO, AND, IMUL and BMUL constraint counts are the true counts; none of them is rounded up
67/// to a power of two. The reductions run over power-of-two-sized operand columns, so the prover
68/// rounds each count up to a power of two and zero-fills the tail when it materializes the column;
69/// [`Self::log_and_constraints`] and its ZERO, IMUL and BMUL siblings report the resulting variable
70/// count. Zero is a valid padding row for every constraint type: an empty operand evaluates to
71/// [`Word::ZERO`], and `0 = 0`, `0 & 0 ^ 0 = 0` and `0 * 0 = 0 || 0` all hold.
72///
73/// # Clone
74///
75/// While this type is cloneable it may be expensive to do so since the constraint systems often
76/// can have millions of constraints.
77#[derive(Debug, Clone)]
78pub struct ConstraintSystem {
79	/// The constants that this constraint system defines.
80	///
81	/// Those constants will be going to be available for constraints in the value vector. Those
82	/// are known to both prover and verifier.
83	pub constants: Vec<Word>,
84	/// The number of input/output values, which are public but chosen per instance.
85	pub n_inout: usize,
86	/// The number of private values, which only the prover knows.
87	pub n_private: usize,
88	/// List of ZERO constraints that must be satisfied by the values vector.
89	pub zero_constraints: Vec<ZeroConstraint>,
90	/// List of AND constraints that must be satisfied by the values vector.
91	pub and_constraints: Vec<AndConstraint>,
92	/// List of IMUL constraints that must be satisfied by the values vector.
93	pub imul_constraints: Vec<ImulConstraint>,
94	/// List of BMUL constraints that must be satisfied by the values vector.
95	pub bmul_constraints: Vec<BmulConstraint>,
96}
97
98impl ConstraintSystem {
99	/// Serialization format version for compatibility checking
100	pub const SERIALIZATION_VERSION: u32 = 10;
101
102	/// The maximum number of values [`Self::validate`] accepts in the inout or private segment.
103	///
104	/// These two counts are declared rather than derived: unlike [`Self::constants`], `n_inout`
105	/// and `n_private` serialize as plain numbers with no backing data, so a payload can claim a
106	/// segment of any size while staying small. `ZKVerifier::setup`, in the binius-verifier
107	/// crate, allocates a word per inout value before reaching anything that would reveal the
108	/// claim as false — so a single four-byte edit to an honest payload is enough to ask for
109	/// gigabytes. Both it and `Verifier::setup` call [`Self::validate`] first, which is what puts
110	/// this bound ahead of the allocation.
111	///
112	/// This is a policy limit rather than a structural one: 2^26 values is 512 MiB per segment.
113	/// The largest circuit in the examples is zklogin, at 302 inout and 259,584 private values,
114	/// so the headroom is 258x on the binding one. It is deliberately sized against the largest
115	/// *intended* circuit rather than the largest present one — a statement aggregating a few
116	/// hundred zklogin-scale proofs approaches the limit, and rejecting a legitimate circuit
117	/// would be worse than the allocation this prevents.
118	///
119	/// Raising it is fine. Note what the ceiling buys, though: it converts an unbounded
120	/// allocation into a bounded one, and 512 MiB per request is still worth pairing with a
121	/// payload-size limit wherever constraint systems arrive from unauthenticated peers.
122	pub const MAX_VALUES_PER_SEGMENT: usize = 1 << 26;
123
124	/// Returns the number of constants.
125	pub const fn n_const(&self) -> usize {
126		self.constants.len()
127	}
128
129	/// Returns the index of the first inout value.
130	pub const fn offset_inout(&self) -> usize {
131		self.n_const()
132	}
133
134	/// Returns the number of public values: the constants and the inout values.
135	pub const fn n_public_values(&self) -> usize {
136		self.n_const() + self.n_inout
137	}
138
139	/// Returns the number of words in the public segment.
140	///
141	/// This is the constants, followed by the inout values when they are placed there.
142	pub const fn n_public_words(&self, inout: InoutSegment) -> usize {
143		match inout {
144			InoutSegment::Public => self.n_public_values(),
145			InoutSegment::Hidden => self.n_const(),
146		}
147	}
148
149	/// Returns the number of word-index variables the public segment spans.
150	///
151	/// The word count need not be a power of two; the reductions read the words past it as zero.
152	pub const fn log_public_words(&self, inout: InoutSegment) -> usize {
153		log2_ceil_usize(self.n_public_words(inout))
154	}
155
156	/// Returns the number of words in the hidden segment.
157	///
158	/// This is the private values, preceded by the inout values when they are placed there.
159	pub const fn n_hidden_words(&self, inout: InoutSegment) -> usize {
160		match inout {
161			InoutSegment::Public => self.n_private,
162			InoutSegment::Hidden => self.n_inout + self.n_private,
163		}
164	}
165
166	/// Returns the number of word-index variables the hidden segment spans.
167	pub const fn log_witness_words(&self, inout: InoutSegment) -> usize {
168		log2_ceil_usize(self.n_hidden_words(inout))
169	}
170
171	/// Returns the number of word-index variables the shift reduction runs over.
172	///
173	/// The reduction addresses both segments with one set of word-index challenges, so it needs
174	/// as many as the wider of the two spans. The narrower segment reads the extra coordinates as
175	/// zero.
176	pub const fn log_segment_words(&self, inout: InoutSegment) -> usize {
177		if self.log_public_words(inout) > self.log_witness_words(inout) {
178			self.log_public_words(inout)
179		} else {
180			self.log_witness_words(inout)
181		}
182	}
183
184	/// Returns the number of values the given segment holds, excluding the padding after them.
185	///
186	/// The scratch segment holds no values a constraint may name, so it reports zero: every index
187	/// into it is out of range as far as this system is concerned.
188	pub const fn segment_len(&self, segment: ValueSegment) -> usize {
189		match segment {
190			ValueSegment::Constant => self.n_const(),
191			ValueSegment::InOut => self.n_inout,
192			ValueSegment::Private => self.n_private,
193			ValueSegment::Scratch => 0,
194		}
195	}
196
197	/// Returns the position of the word a [`ValueIndex`] names within the value vector.
198	///
199	/// This is the address the proving protocol reads the word at: the constants, then the inout
200	/// values, then the private ones. Where the segment boundary falls does not enter, so the
201	/// address is the same under either [`InoutSegment`] placement. Scratch words are not part of a
202	/// constraint system, so a scratch index lands past the last word — [`Self::validate`] rejects
203	/// any constraint naming one.
204	pub const fn word_offset(&self, index: ValueIndex) -> usize {
205		let segment_start = match index.segment() {
206			ValueSegment::Constant => 0,
207			ValueSegment::InOut => self.offset_inout(),
208			ValueSegment::Private => self.n_public_values(),
209			ValueSegment::Scratch => self.value_vec_len(),
210		};
211		segment_start + index.index() as usize
212	}
213
214	/// Builds a value vector from the inout values and the private values.
215	///
216	/// The constants come from the system itself, so a caller supplies only what varies per
217	/// instance — the same split [`Self::validate`] enforces and the verifier takes.
218	pub fn value_vec_from_data(&self, inout: &[Word], private: &[Word]) -> ValueVec {
219		let public = [self.constants.as_slice(), inout].concat();
220		ValueVec::new_from_data(self.n_const(), &public, private)
221	}
222
223	/// Ensures that this constraint system is well-formed and ready for proving.
224	///
225	/// Specifically checks that:
226	///
227	/// - the declared segment sizes are within [`Self::MAX_VALUES_PER_SEGMENT`].
228	/// - every [shifted value index][super::ShiftedValueIndex] is canonical.
229	/// - referenced value indices are within their segment.
230	/// - constraints do not reference scratch values.
231	/// - shifts amounts are valid.
232	/// - a lone shift sits in the inner slot of its shift sequence.
233	/// - a genuine shift pair does not collapse to one shift, nor clear the word.
234	pub fn validate(&self) -> Result<(), ConstraintSystemError> {
235		tracing::debug_span!("Validating constraint system");
236
237		// Bound the two declared counts before anything reads them; see
238		// `MAX_VALUES_PER_SEGMENT`. The constants need no bound of their own: they are backed by
239		// the words in the payload, so claiming more of them costs the sender proportionally.
240		for segment in [ValueSegment::InOut, ValueSegment::Private] {
241			let len = self.segment_len(segment);
242			if len > Self::MAX_VALUES_PER_SEGMENT {
243				return Err(ConstraintSystemError::SegmentTooLarge { segment, len });
244			}
245		}
246
247		self.validate_constraints(
248			&self.zero_constraints,
249			ZeroConstraint::KIND,
250			ZeroConstraint::OPERAND_NAMES,
251		)?;
252		self.validate_constraints(
253			&self.and_constraints,
254			AndConstraint::KIND,
255			AndConstraint::OPERAND_NAMES,
256		)?;
257		self.validate_constraints(
258			&self.imul_constraints,
259			ImulConstraint::KIND,
260			ImulConstraint::OPERAND_NAMES,
261		)?;
262		self.validate_constraints(
263			&self.bmul_constraints,
264			BmulConstraint::KIND,
265			BmulConstraint::OPERAND_NAMES,
266		)?;
267
268		Ok(())
269	}
270
271	/// Checks that a value vector satisfies this constraint system.
272	///
273	/// Specifically checks that:
274	///
275	/// - the value vector opens the declared constants to their declared words.
276	/// - every constraint holds, in kind order: zero, then AND, then IMUL, then BMUL.
277	///
278	/// Operands are evaluated one word at a time, directly off the value vector.
279	/// That makes this the reference the prover's packed evaluation is checked against.
280	///
281	/// # Errors
282	///
283	/// Reports the first failure found, in the order listed above.
284	/// A reported constraint position counts within that constraint's own kind.
285	pub fn verify(&self, values: &ValueVec) -> Result<(), VerificationError> {
286		// Constraints read constants through the value vector.
287		// A vector opening one to the wrong word satisfies a different system than declared.
288		for (index, &constant) in self.constants.iter().enumerate() {
289			let value_index = index as u32;
290			let actual = values[ValueIndex::constant(value_index)];
291			if actual != constant {
292				return Err(VerificationError::ConstantMismatch {
293					value_index,
294					expected: constant.as_u64(),
295					actual: actual.as_u64(),
296				});
297			}
298		}
299
300		// Each kind is numbered from zero, so a position is only meaningful with its kind.
301		// The violation carries that kind, which is what the reported message prints.
302		for (constraint_index, constraint) in self.zero_constraints.iter().enumerate() {
303			constraint
304				.verify(values)
305				.map_err(|source| VerificationError::Unsatisfied {
306					constraint_index,
307					source,
308				})?;
309		}
310		for (constraint_index, constraint) in self.and_constraints.iter().enumerate() {
311			constraint
312				.verify(values)
313				.map_err(|source| VerificationError::Unsatisfied {
314					constraint_index,
315					source,
316				})?;
317		}
318		for (constraint_index, constraint) in self.imul_constraints.iter().enumerate() {
319			constraint
320				.verify(values)
321				.map_err(|source| VerificationError::Unsatisfied {
322					constraint_index,
323					source,
324				})?;
325		}
326		for (constraint_index, constraint) in self.bmul_constraints.iter().enumerate() {
327			constraint
328				.verify(values)
329				.map_err(|source| VerificationError::Unsatisfied {
330					constraint_index,
331					source,
332				})?;
333		}
334
335		Ok(())
336	}
337
338	/// Checks every operand of every constraint of one kind, in storage order.
339	fn validate_constraints<C: AsRef<[Operand; ARITY]>, const ARITY: usize>(
340		&self,
341		constraints: &[C],
342		constraint_kind: ConstraintKind,
343		operand_names: [&'static str; ARITY],
344	) -> Result<(), ConstraintSystemError> {
345		for (i, constraint) in constraints.iter().enumerate() {
346			for (operand, name) in iter::zip(constraint.as_ref(), operand_names) {
347				self.validate_operand(operand, constraint_kind, i, name)?;
348			}
349		}
350		Ok(())
351	}
352
353	/// Checks that every term of an operand is canonical and references a value word.
354	fn validate_operand(
355		&self,
356		operand: &Operand,
357		constraint_kind: ConstraintKind,
358		constraint_index: usize,
359		operand_name: &'static str,
360	) -> Result<(), ConstraintSystemError> {
361		self.operand_fault(operand).map_or(Ok(()), |source| {
362			Err(ConstraintSystemError::ConstraintOperand {
363				constraint_kind,
364				constraint_index,
365				operand_name,
366				source,
367			})
368		})
369	}
370
371	/// Returns the first way a term of an operand is malformed, or `None` when every term is
372	/// well-formed.
373	///
374	/// The fault says nothing about where the operand sits, so a constraint operand and a chip-call
375	/// operand can both report it under their own naming.
376	pub fn operand_fault(&self, operand: &Operand) -> Option<OperandFault> {
377		operand.iter().find_map(|term| {
378			for shift in term.shift_seq {
379				// check canonicity. SLL is the canonical form of the identity.
380				if !shift.is_canonical() {
381					return Some(OperandFault::NonCanonicalShift);
382				}
383				// Half-word (*32) variants cap at 32, full-width at 64. `Shift::new` and the
384				// deserializer both enforce this, but the fields are public, so a hand-built term
385				// can still carry an amount the variant cannot represent.
386				let max_amount = shift.variant.max_amount();
387				if usize::from(shift.amount) >= max_amount {
388					return Some(OperandFault::ShiftAmountTooLarge {
389						shift_amount: shift.amount as usize,
390						max_amount,
391					});
392				}
393			}
394			// A lone shift belongs in the inner slot, so an identity there settles the outer one.
395			// Were the outer slot allowed to carry the lone shift, one map would have two
396			// spellings and two terms denoting the same shifted word would not compare equal.
397			if term.is_unshifted() && term.is_doubly_shifted() {
398				return Some(OperandFault::NonCanonicalShiftSequence);
399			}
400			// A genuine pair must not collapse. `Single` means the frontend failed to merge two
401			// shifts that compose into one; `Zero` means it emitted a term whose every bit is
402			// cleared, which should have been deleted rather than encoded.
403			if term.is_doubly_shifted() {
404				let composition = Shift::compose(term.inner(), term.outer());
405				if composition != Composition::Pair {
406					return Some(OperandFault::CollapsibleShiftSequence { composition });
407				}
408			}
409			// Scratch words are uncommitted temporaries of the circuit that produced this system,
410			// so no constraint may name one.
411			let segment = term.value_index.segment();
412			if !segment.is_referenceable() {
413				return Some(OperandFault::ScratchValueIndex);
414			}
415			// An index is checked against its own segment, so it can only name a declared value.
416			let segment_len = self.segment_len(segment);
417			if term.value_index.index() as usize >= segment_len {
418				return Some(OperandFault::OutOfRangeValueIndex {
419					segment,
420					value_index: term.value_index.index(),
421					segment_len,
422				});
423			}
424			None
425		})
426	}
427
428	/// Returns the number of ZERO constraints in the system.
429	pub const fn n_zero_constraints(&self) -> usize {
430		self.zero_constraints.len()
431	}
432
433	/// Returns the number of AND constraints in the system.
434	pub const fn n_and_constraints(&self) -> usize {
435		self.and_constraints.len()
436	}
437
438	/// Returns the number of IMUL  constraints in the system.
439	pub const fn n_imul_constraints(&self) -> usize {
440		self.imul_constraints.len()
441	}
442
443	/// Returns the number of BMUL constraints in the system.
444	pub const fn n_bmul_constraints(&self) -> usize {
445		self.bmul_constraints.len()
446	}
447
448	/// Returns the number of variables the Zero reduction runs over, or `None` when the system has
449	/// no ZERO constraints.
450	///
451	/// This is `ceil(log2(n_zero_constraints))`, matching the zero-padded operand column the
452	/// reduction consumes. As with [`Self::log_and_constraints`], the Zero reduction always runs: a
453	/// system with no ZERO constraints still gets a single all-zero row, which the constraint
454	/// vacuously satisfies. Such a system reduces over zero variables, so its callers read `None`
455	/// as zero.
456	pub const fn log_zero_constraints(&self) -> Option<usize> {
457		match self.n_zero_constraints() {
458			0 => None,
459			n => Some(log2_ceil_usize(n)),
460		}
461	}
462
463	/// Returns the number of variables the BitAnd reduction runs over, or `None` when the system
464	/// has no AND constraints.
465	///
466	/// The reduction operates on operand columns with one row per AND constraint, zero-padded up
467	/// to a power of two, so it has `ceil(log2(n_and_constraints))` variables. Unlike the two
468	/// multiplication reductions, the BitAnd reduction always runs: a system with no AND
469	/// constraints still gets a single all-zero row, which every constraint type satisfies. Such a
470	/// system reduces over zero variables, so its callers read `None` as zero.
471	pub const fn log_and_constraints(&self) -> Option<usize> {
472		match self.n_and_constraints() {
473			0 => None,
474			n => Some(log2_ceil_usize(n)),
475		}
476	}
477
478	/// Returns the number of variables the IntMul reduction runs over, or `None` when the system
479	/// has no IMUL constraints.
480	///
481	/// This is `ceil(log2(n_imul_constraints))`, matching the zero-padded operand columns the
482	/// reduction consumes. `None` is the skip signal: an empty IMUL set makes the prover and
483	/// verifier skip the IntMul reduction entirely, rather than run it over a single dummy
484	/// constraint.
485	pub const fn log_imul_constraints(&self) -> Option<usize> {
486		match self.n_imul_constraints() {
487			0 => None,
488			n => Some(log2_ceil_usize(n)),
489		}
490	}
491
492	/// Returns the number of variables the BinMul reduction runs over, or `None` when the system
493	/// has no BMUL constraints.
494	///
495	/// This is `ceil(log2(n_bmul_constraints))`, matching the zero-padded operand columns the
496	/// reduction consumes. As with [`Self::log_imul_constraints`], `None` is the skip signal; both
497	/// sides skip the BinMul reduction for an empty BMUL set.
498	pub const fn log_bmul_constraints(&self) -> Option<usize> {
499		match self.n_bmul_constraints() {
500			0 => None,
501			n => Some(log2_ceil_usize(n)),
502		}
503	}
504
505	/// The total length of the [`ValueVec`] expected by this constraint system.
506	pub const fn value_vec_len(&self) -> usize {
507		self.n_public_values() + self.n_private
508	}
509}
510
511impl SerializeBytes for ConstraintSystem {
512	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
513		Self::SERIALIZATION_VERSION.serialize(&mut write_buf)?;
514
515		self.constants.serialize(&mut write_buf)?;
516		self.n_inout.serialize(&mut write_buf)?;
517		self.n_private.serialize(&mut write_buf)?;
518		self.zero_constraints.serialize(&mut write_buf)?;
519		self.and_constraints.serialize(&mut write_buf)?;
520		self.imul_constraints.serialize(&mut write_buf)?;
521		self.bmul_constraints.serialize(write_buf)
522	}
523}
524
525impl DeserializeBytes for ConstraintSystem {
526	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
527	where
528		Self: Sized,
529	{
530		let version = u32::deserialize(&mut read_buf)?;
531		if version != Self::SERIALIZATION_VERSION {
532			return Err(SerializationError::InvalidConstruction {
533				name: "ConstraintSystem::version",
534			});
535		}
536
537		let constants = Vec::<Word>::deserialize(&mut read_buf)?;
538		let n_inout = usize::deserialize(&mut read_buf)?;
539		let n_private = usize::deserialize(&mut read_buf)?;
540		let zero_constraints = Vec::<ZeroConstraint>::deserialize(&mut read_buf)?;
541		let and_constraints = Vec::<AndConstraint>::deserialize(&mut read_buf)?;
542		let imul_constraints = Vec::<ImulConstraint>::deserialize(&mut read_buf)?;
543		let bmul_constraints = Vec::<BmulConstraint>::deserialize(read_buf)?;
544
545		Ok(ConstraintSystem {
546			constants,
547			n_inout,
548			n_private,
549			zero_constraints,
550			and_constraints,
551			imul_constraints,
552			bmul_constraints,
553		})
554	}
555}
556
557#[cfg(test)]
558mod tests {
559	use super::*;
560	use crate::{
561		constraint_system::{Shift, ShiftVariant, ShiftedValueIndex, ValuesData, ValuesRef},
562		error::ConstraintViolation,
563	};
564
565	/// A shape with one padding word after the constants and two after the inout values, so the
566	/// public segment is 8 words, followed by a hidden segment of 6 values and 2 padding words.
567	///
568	///     [ c c c _ | i i _ _ ][ p p p p p p _ _ ]
569	///       0 1 2 3   4 5 6 7   8 ...      13
570	fn test_shape() -> ConstraintSystem {
571		ConstraintSystem {
572			constants: vec![
573				Word::from_u64(1),
574				Word::from_u64(42),
575				Word::from_u64(0xDEADBEEF),
576			],
577			n_inout: 2,
578			n_private: 6,
579			zero_constraints: vec![],
580			and_constraints: vec![],
581			imul_constraints: vec![],
582			bmul_constraints: vec![],
583		}
584	}
585
586	pub(crate) fn create_test_constraint_system() -> ConstraintSystem {
587		ConstraintSystem {
588			zero_constraints: vec![ZeroConstraint::plain([
589				ValueIndex::constant(0),
590				ValueIndex::inout(0),
591				ValueIndex::private(0),
592			])],
593			and_constraints: vec![
594				AndConstraint::plain_abc(
595					vec![ValueIndex::constant(0), ValueIndex::constant(1)],
596					vec![ValueIndex::constant(2)],
597					vec![ValueIndex::inout(0), ValueIndex::inout(1)],
598				),
599				AndConstraint::abc(
600					vec![ShiftedValueIndex::sll(ValueIndex::constant(0), 5)],
601					vec![ShiftedValueIndex::srl(ValueIndex::constant(1), 10)],
602					vec![ShiftedValueIndex::sar(ValueIndex::constant(2), 15)],
603				),
604			],
605			imul_constraints: vec![ImulConstraint([
606				vec![ShiftedValueIndex::plain(ValueIndex::constant(0))],
607				vec![ShiftedValueIndex::plain(ValueIndex::constant(1))],
608				vec![ShiftedValueIndex::plain(ValueIndex::constant(2))],
609				vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
610			])],
611			bmul_constraints: vec![BmulConstraint([
612				vec![ShiftedValueIndex::plain(ValueIndex::constant(0))],
613				vec![ShiftedValueIndex::plain(ValueIndex::constant(1))],
614				vec![ShiftedValueIndex::plain(ValueIndex::constant(2))],
615				vec![ShiftedValueIndex::plain(ValueIndex::inout(0))],
616				vec![ShiftedValueIndex::plain(ValueIndex::inout(1))],
617				vec![ShiftedValueIndex::sll(ValueIndex::constant(0), 5)],
618			])],
619			..test_shape()
620		}
621	}
622
623	#[test]
624	fn test_constraint_system_serialization_round_trip() {
625		let original = create_test_constraint_system();
626
627		let mut buf = Vec::new();
628		original.serialize(&mut buf).unwrap();
629
630		let deserialized = ConstraintSystem::deserialize(&mut buf.as_slice()).unwrap();
631
632		// Check version
633		assert_eq!(ConstraintSystem::SERIALIZATION_VERSION, 10);
634
635		// Check the value vector shape
636		assert_eq!(original.constants, deserialized.constants);
637		assert_eq!(original.n_inout, deserialized.n_inout);
638		assert_eq!(original.n_private, deserialized.n_private);
639
640		// Check zero_constraints
641		assert_eq!(original.zero_constraints.len(), deserialized.zero_constraints.len());
642
643		// Check and_constraints
644		assert_eq!(original.and_constraints.len(), deserialized.and_constraints.len());
645
646		// Check imul_constraints
647		assert_eq!(original.imul_constraints.len(), deserialized.imul_constraints.len());
648
649		// Check bmul_constraints
650		assert_eq!(original.bmul_constraints.len(), deserialized.bmul_constraints.len());
651	}
652
653	#[test]
654	fn test_constraint_system_version_mismatch() {
655		// Create a buffer with wrong version
656		let mut buf = Vec::new();
657		999u32.serialize(&mut buf).unwrap(); // Wrong version
658
659		let result = ConstraintSystem::deserialize(&mut buf.as_slice());
660		assert!(result.is_err());
661		match result.unwrap_err() {
662			SerializationError::InvalidConstruction { name } => {
663				assert_eq!(name, "ConstraintSystem::version");
664			}
665			_ => panic!("Expected InvalidConstruction error"),
666		}
667	}
668
669	#[test]
670	fn test_serialization_with_different_sources() {
671		let original = create_test_constraint_system();
672
673		// Test with Vec<u8> (memory buffer)
674		let mut vec_buf = Vec::new();
675		original.serialize(&mut vec_buf).unwrap();
676		let deserialized1 = ConstraintSystem::deserialize(&mut vec_buf.as_slice()).unwrap();
677		assert_eq!(original.constants.len(), deserialized1.constants.len());
678
679		// Test with bytes::BytesMut (another common buffer type)
680		let mut bytes_buf = bytes::BytesMut::new();
681		original.serialize(&mut bytes_buf).unwrap();
682		let deserialized2 = ConstraintSystem::deserialize(bytes_buf.freeze()).unwrap();
683		assert_eq!(original.constants.len(), deserialized2.constants.len());
684	}
685
686	/// Helper function to create or update the reference binary file for version compatibility
687	/// testing. This is not run automatically but can be used to regenerate the reference file
688	/// when needed.
689	#[test]
690	#[ignore] // Use `cargo test -- --ignored create_reference_binary` to run this
691	fn create_reference_binary_file() {
692		let constraint_system = create_test_constraint_system();
693
694		// Serialize to binary data
695		let mut buf = Vec::new();
696		constraint_system.serialize(&mut buf).unwrap();
697
698		// Write to reference file.
699		let test_data_path = std::path::Path::new("test_data/constraint_system_v10.bin");
700
701		// Create directory if it doesn't exist
702		if let Some(parent) = test_data_path.parent() {
703			std::fs::create_dir_all(parent).unwrap();
704		}
705
706		std::fs::write(test_data_path, &buf).unwrap();
707
708		println!("Created reference binary file at: {:?}", test_data_path);
709		println!("Binary data length: {} bytes", buf.len());
710	}
711
712	/// Test deserialization from a reference binary file to ensure version compatibility.
713	/// This test will fail if breaking changes are made without incrementing the version.
714	#[test]
715	fn test_deserialize_from_reference_binary_file() {
716		// The v10 format widens every shifted value index to a sequence of two shifts, so it
717		// carries one extra byte pair per term. Older files spell one shift per term and no
718		// longer parse.
719		let binary_data = include_bytes!("../../test_data/constraint_system_v10.bin");
720
721		let deserialized = ConstraintSystem::deserialize(&mut binary_data.as_slice()).unwrap();
722
723		assert_eq!(deserialized.n_const(), 3);
724		assert_eq!(deserialized.n_inout, 2);
725		assert_eq!(deserialized.n_private, 6);
726
727		assert_eq!(deserialized.constants[0].as_u64(), 1);
728		assert_eq!(deserialized.constants[1].as_u64(), 42);
729		assert_eq!(deserialized.constants[2].as_u64(), 0xDEADBEEF);
730
731		assert_eq!(deserialized.zero_constraints.len(), 1);
732		assert_eq!(deserialized.and_constraints.len(), 2);
733		assert_eq!(deserialized.imul_constraints.len(), 1);
734		assert_eq!(deserialized.bmul_constraints.len(), 1);
735
736		// Verify that the version is what we expect
737		// This is implicitly checked during deserialization, but we can also verify
738		// the file starts with the correct version bytes
739		let version_bytes = &binary_data[0..4]; // First 4 bytes should be version
740		let expected_version_bytes = 10u32.to_le_bytes(); // Version 10 in little-endian
741		assert_eq!(
742			version_bytes, expected_version_bytes,
743			"Binary file version mismatch. If you made breaking changes, increment ConstraintSystem::SERIALIZATION_VERSION"
744		);
745	}
746
747	#[test]
748	fn test_log_witness_words() {
749		let cs = |n_private: usize| ConstraintSystem {
750			n_private,
751			..test_shape()
752		};
753		// Typical: more private values than public words, rounded up to a power of two.
754		assert_eq!(cs(60).log_witness_words(InoutSegment::Public), 6);
755		// Exact power-of-two private count.
756		assert_eq!(cs(32).log_witness_words(InoutSegment::Public), 5);
757	}
758
759	#[test]
760	fn segment_lengths_are_the_value_counts() {
761		// Three constants and two inout values are five public words; six private values are six
762		// hidden words. Neither is padded.
763		let cs = test_shape();
764		assert_eq!(cs.n_public_values(), 5);
765		assert_eq!(cs.n_public_words(InoutSegment::Public), 5);
766		assert_eq!(cs.n_hidden_words(InoutSegment::Public), 6);
767		assert_eq!(cs.value_vec_len(), 11);
768
769		// The spans are the counts rounded up, and the reduction runs over the wider of the two.
770		assert_eq!(cs.log_public_words(InoutSegment::Public), 3);
771		assert_eq!(cs.log_witness_words(InoutSegment::Public), 3);
772		assert_eq!(cs.log_segment_words(InoutSegment::Public), 3);
773
774		// A hidden segment wider than the public one sets the span.
775		let wide = ConstraintSystem {
776			n_private: 60,
777			..test_shape()
778		};
779		assert_eq!(wide.log_public_words(InoutSegment::Public), 3);
780		assert_eq!(wide.log_witness_words(InoutSegment::Public), 6);
781		assert_eq!(wide.log_segment_words(InoutSegment::Public), 6);
782
783		// And a public segment wider than the hidden one sets it instead — the case the old
784		// hidden-segment padding existed to rule out.
785		let public_heavy = ConstraintSystem {
786			n_inout: 200,
787			n_private: 4,
788			..test_shape()
789		};
790		assert_eq!(public_heavy.log_public_words(InoutSegment::Public), 8);
791		assert_eq!(public_heavy.log_witness_words(InoutSegment::Public), 2);
792		assert_eq!(public_heavy.log_segment_words(InoutSegment::Public), 8);
793	}
794
795	#[test]
796	fn hidden_inout_moves_the_segment_boundary() {
797		// The same shape as above, read with the inout values in the hidden segment: three
798		// constants are the whole public segment, and the two inout values join the six private
799		// ones.
800		let cs = test_shape();
801		assert_eq!(cs.n_public_words(InoutSegment::Hidden), 3);
802		assert_eq!(cs.n_hidden_words(InoutSegment::Hidden), 8);
803
804		// The value vector is the same either way, so the word addresses are too.
805		assert_eq!(cs.value_vec_len(), 11);
806		assert_eq!(cs.word_offset(ValueIndex::inout(0)), 3);
807		assert_eq!(cs.word_offset(ValueIndex::private(0)), 5);
808	}
809
810	#[test]
811	fn test_validate_rejects_scratch_references() {
812		let mut cs = test_shape();
813
814		// Scratch words are the evaluating circuit's uncommitted temporaries, so a system that
815		// names one references a word that was never committed.
816		cs.and_constraints.push(AndConstraint::plain_abc(
817			vec![ValueIndex::constant(0)],
818			vec![ValueIndex::scratch(0)], // SCRATCH!
819			vec![ValueIndex::private(0)],
820		));
821
822		match cs.validate().unwrap_err() {
823			ConstraintSystemError::ConstraintOperand {
824				constraint_kind,
825				source: OperandFault::ScratchValueIndex,
826				..
827			} => {
828				assert_eq!(constraint_kind, ConstraintKind::And);
829			}
830			other => panic!("Expected ScratchValueIndex error, got: {:?}", other),
831		}
832	}
833
834	#[test]
835	fn test_validate_checks_each_segment_against_its_own_length() {
836		// The shape holds 3 constants, 2 inout values and 6 private values. Index 3 is out of
837		// range in the constant segment while naming a perfectly valid private word, which is
838		// what makes the check segment-relative rather than global.
839		let mut cs = test_shape();
840		cs.and_constraints.push(AndConstraint::plain_abc(
841			vec![ValueIndex::constant(3)],
842			vec![ValueIndex::inout(0)],
843			vec![ValueIndex::private(3)],
844		));
845
846		match cs.validate().unwrap_err() {
847			ConstraintSystemError::ConstraintOperand {
848				source:
849					OperandFault::OutOfRangeValueIndex {
850						segment,
851						value_index,
852						segment_len,
853					},
854				..
855			} => {
856				assert_eq!(segment, ValueSegment::Constant);
857				assert_eq!(value_index, 3);
858				assert_eq!(segment_len, 3);
859			}
860			other => panic!("Expected OutOfRangeValueIndex error, got: {:?}", other),
861		}
862	}
863
864	#[test]
865	fn test_validate_accepts_non_padding_references() {
866		let mut cs = test_shape();
867
868		// Add constraints that only reference valid non-padding indices
869		cs.and_constraints.push(AndConstraint::plain_abc(
870			vec![ValueIndex::constant(0), ValueIndex::constant(1)], // constants
871			vec![ValueIndex::inout(0), ValueIndex::inout(1)],       // inout
872			vec![ValueIndex::private(0), ValueIndex::private(1)],   // private
873		));
874
875		cs.imul_constraints.push(ImulConstraint([
876			vec![ShiftedValueIndex::plain(ValueIndex::private(2))], // a
877			vec![ShiftedValueIndex::plain(ValueIndex::private(3))], // b
878			vec![ShiftedValueIndex::plain(ValueIndex::private(4))], // lo
879			vec![ShiftedValueIndex::plain(ValueIndex::private(5))], // hi
880		]));
881
882		let result = cs.validate();
883		assert!(
884			result.is_ok(),
885			"Should accept constraints with only valid references: {:?}",
886			result
887		);
888	}
889
890	#[test]
891	fn test_validate_keeps_true_constraint_counts() {
892		let cs = create_test_constraint_system();
893		let (n_zero, n_and, n_imul, n_bmul) = (
894			cs.n_zero_constraints(),
895			cs.n_and_constraints(),
896			cs.n_imul_constraints(),
897			cs.n_bmul_constraints(),
898		);
899		cs.validate().unwrap();
900		assert_eq!(cs.n_zero_constraints(), n_zero);
901		assert_eq!(cs.n_and_constraints(), n_and);
902		assert_eq!(cs.n_imul_constraints(), n_imul);
903		assert_eq!(cs.n_bmul_constraints(), n_bmul);
904	}
905
906	#[test]
907	fn test_validate_rejects_out_of_range_in_zero_constraint() {
908		let mut cs = test_shape();
909
910		cs.zero_constraints
911			.push(ZeroConstraint::plain([ValueIndex::constant(0), ValueIndex::private(100)]));
912
913		match cs.validate().unwrap_err() {
914			ConstraintSystemError::ConstraintOperand {
915				constraint_kind,
916				operand_name,
917				source:
918					OperandFault::OutOfRangeValueIndex {
919						value_index,
920						segment_len,
921						..
922					},
923				..
924			} => {
925				assert_eq!(constraint_kind, ConstraintKind::Zero);
926				assert_eq!(operand_name, "val");
927				assert_eq!(value_index, 100);
928				assert_eq!(segment_len, 6);
929			}
930			other => panic!("Expected OutOfRangeValueIndex error, got: {:?}", other),
931		}
932	}
933
934	#[test]
935	fn test_log_constraint_counts_round_up() {
936		let mut cs = test_shape();
937		// An empty set reports `None`; the BitAnd reduction reads that as its single all-zero
938		// padding row, i.e. zero variables.
939		assert_eq!(cs.log_and_constraints(), None);
940		assert_eq!(cs.log_zero_constraints(), None);
941
942		cs.zero_constraints =
943			vec![ZeroConstraint::plain([ValueIndex::constant(0), ValueIndex::private(0)]); 3];
944		assert_eq!(cs.log_zero_constraints(), Some(2));
945
946		let and = AndConstraint::plain_abc(
947			vec![ValueIndex::constant(0)],
948			vec![ValueIndex::inout(0)],
949			vec![ValueIndex::private(0)],
950		);
951		cs.and_constraints = vec![and; 3];
952		assert_eq!(cs.log_and_constraints(), Some(2));
953		cs.and_constraints.push(cs.and_constraints[0].clone());
954		assert_eq!(cs.log_and_constraints(), Some(2));
955	}
956
957	#[test]
958	fn test_validate_rejects_out_of_range_indices() {
959		let mut cs = test_shape();
960
961		// Add AND constraint that references an out-of-range index
962		cs.and_constraints.push(AndConstraint::plain_abc(
963			vec![ValueIndex::constant(0)], // valid constant
964			vec![ValueIndex::private(6)],  // OUT OF RANGE! the private segment holds 6 values
965			vec![ValueIndex::private(0)],  // valid private value
966		));
967
968		let result = cs.validate();
969		assert!(result.is_err(), "Should reject constraint with out-of-range index");
970
971		match result.unwrap_err() {
972			ConstraintSystemError::ConstraintOperand {
973				constraint_kind,
974				operand_name,
975				source:
976					OperandFault::OutOfRangeValueIndex {
977						value_index,
978						segment_len,
979						..
980					},
981				..
982			} => {
983				assert_eq!(constraint_kind, ConstraintKind::And);
984				assert_eq!(operand_name, "b");
985				assert_eq!(value_index, 6);
986				assert_eq!(segment_len, 6);
987			}
988			other => panic!("Expected OutOfRangeValueIndex error, got: {:?}", other),
989		}
990	}
991
992	#[test]
993	fn test_validate_rejects_out_of_range_in_imul_constraint() {
994		let mut cs = test_shape();
995
996		// Add IMUL constraint with out-of-range index in 'hi' operand
997		cs.imul_constraints.push(ImulConstraint([
998			vec![ShiftedValueIndex::plain(ValueIndex::constant(0))], // a: valid
999			vec![ShiftedValueIndex::plain(ValueIndex::constant(1))], // b: valid
1000			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],  // lo: valid
1001			vec![ShiftedValueIndex::plain(ValueIndex::private(100))], // hi: WAY out of range!
1002		]));
1003
1004		let result = cs.validate();
1005		assert!(result.is_err(), "Should reject IMUL constraint with out-of-range index");
1006
1007		match result.unwrap_err() {
1008			ConstraintSystemError::ConstraintOperand {
1009				constraint_kind,
1010				operand_name,
1011				source:
1012					OperandFault::OutOfRangeValueIndex {
1013						value_index,
1014						segment_len,
1015						..
1016					},
1017				..
1018			} => {
1019				assert_eq!(constraint_kind, ConstraintKind::Imul);
1020				assert_eq!(operand_name, "hi");
1021				assert_eq!(value_index, 100);
1022				assert_eq!(segment_len, 6);
1023			}
1024			other => panic!("Expected OutOfRangeValueIndex error, got: {:?}", other),
1025		}
1026	}
1027
1028	#[test]
1029	fn test_validate_rejects_out_of_range_in_bmul_constraint() {
1030		let mut cs = test_shape();
1031
1032		// Add BMUL constraint with out-of-range index in 'c_hi' operand
1033		cs.bmul_constraints.push(BmulConstraint([
1034			vec![ShiftedValueIndex::plain(ValueIndex::constant(0))], // a_lo: valid const
1035			vec![ShiftedValueIndex::plain(ValueIndex::inout(0))],    // a_hi: valid inout
1036			vec![ShiftedValueIndex::plain(ValueIndex::inout(1))],    // b_lo: valid inout
1037			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],  // b_hi: valid private
1038			vec![ShiftedValueIndex::plain(ValueIndex::private(1))],  // c_lo: valid private
1039			vec![ShiftedValueIndex::plain(ValueIndex::private(100))], // c_hi: WAY out of range!
1040		]));
1041
1042		let result = cs.validate();
1043		assert!(result.is_err(), "Should reject BMUL constraint with out-of-range index");
1044
1045		match result.unwrap_err() {
1046			ConstraintSystemError::ConstraintOperand {
1047				constraint_kind,
1048				operand_name,
1049				source:
1050					OperandFault::OutOfRangeValueIndex {
1051						value_index,
1052						segment_len,
1053						..
1054					},
1055				..
1056			} => {
1057				assert_eq!(constraint_kind, ConstraintKind::Bmul);
1058				assert_eq!(operand_name, "c_hi");
1059				assert_eq!(value_index, 100);
1060				assert_eq!(segment_len, 6);
1061			}
1062			other => panic!("Expected OutOfRangeValueIndex error, got: {:?}", other),
1063		}
1064	}
1065
1066	#[test]
1067	fn test_validate_rejects_half_word_shift_amount_out_of_range() {
1068		let mut cs = test_shape();
1069
1070		// A half-word (*32) shift may only use amounts < 32.
1071		// 32 is out of range even though it is below the full-width bound of 64.
1072		cs.and_constraints.push(AndConstraint::abc(
1073			vec![ShiftedValueIndex::single(
1074				ValueIndex::constant(0),
1075				// Built raw: `Shift::new` would reject this amount, and `validate` is what is
1076				// under test here.
1077				Shift {
1078					variant: ShiftVariant::Sll32,
1079					amount: 32,
1080				},
1081			)],
1082			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1083			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1084		));
1085
1086		match cs.validate().unwrap_err() {
1087			ConstraintSystemError::ConstraintOperand {
1088				constraint_kind,
1089				constraint_index,
1090				operand_name,
1091				source:
1092					OperandFault::ShiftAmountTooLarge {
1093						shift_amount,
1094						max_amount,
1095					},
1096			} => {
1097				assert_eq!(constraint_kind, ConstraintKind::And);
1098				assert_eq!(constraint_index, 0);
1099				assert_eq!(operand_name, "a");
1100				assert_eq!(shift_amount, 32);
1101				assert_eq!(max_amount, 32);
1102			}
1103			other => panic!("Expected ShiftAmountTooLarge, got: {:?}", other),
1104		}
1105	}
1106
1107	#[test]
1108	fn test_validate_checks_the_outer_shift_slot_too() {
1109		let mut cs = test_shape();
1110
1111		// The bound applies to both slots, so an outer half-word shift is checked the same way.
1112		// A pair whose inner shift is fine still fails on the outer one.
1113		cs.and_constraints.push(AndConstraint::abc(
1114			vec![ShiftedValueIndex::new(
1115				ValueIndex::constant(0),
1116				[
1117					Shift::srl(3),
1118					// Built raw: `Shift::new` would reject this amount before `validate` sees it.
1119					Shift {
1120						variant: ShiftVariant::Sll32,
1121						amount: 32,
1122					},
1123				],
1124			)],
1125			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1126			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1127		));
1128
1129		match cs.validate().unwrap_err() {
1130			ConstraintSystemError::ConstraintOperand {
1131				constraint_kind,
1132				constraint_index,
1133				operand_name,
1134				source:
1135					OperandFault::ShiftAmountTooLarge {
1136						shift_amount,
1137						max_amount,
1138					},
1139			} => {
1140				assert_eq!(constraint_kind, ConstraintKind::And);
1141				assert_eq!(constraint_index, 0);
1142				assert_eq!(operand_name, "a");
1143				assert_eq!(shift_amount, 32);
1144				assert_eq!(max_amount, 32);
1145			}
1146			other => panic!("Expected ShiftAmountTooLarge, got: {:?}", other),
1147		}
1148	}
1149
1150	#[test]
1151	fn test_validate_rejects_a_lone_shift_in_the_outer_slot() {
1152		let mut cs = test_shape();
1153
1154		// The canonical form places a lone shift inner. Spelling it outer denotes the same map
1155		// through a second spelling, so two terms on the same shifted word would not compare equal.
1156		cs.and_constraints.push(AndConstraint::abc(
1157			vec![ShiftedValueIndex::new(
1158				ValueIndex::constant(0),
1159				[Shift::IDENTITY, Shift::rotr(5)],
1160			)],
1161			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1162			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1163		));
1164
1165		match cs.validate().unwrap_err() {
1166			ConstraintSystemError::ConstraintOperand {
1167				constraint_kind,
1168				constraint_index,
1169				operand_name,
1170				source: OperandFault::NonCanonicalShiftSequence,
1171			} => {
1172				assert_eq!(constraint_kind, ConstraintKind::And);
1173				assert_eq!(constraint_index, 0);
1174				assert_eq!(operand_name, "a");
1175			}
1176			other => panic!("Expected NonCanonicalShiftSequence, got: {:?}", other),
1177		}
1178	}
1179
1180	#[test]
1181	fn test_validate_rejects_a_pair_that_collapses_to_one_shift() {
1182		let mut cs = test_shape();
1183
1184		// Two rotations of one variant chain, so this pair denotes `rotr(9)` alone. Accepting it
1185		// would spend a shift slot the reduction has to pay for on a map that needs only one.
1186		cs.and_constraints.push(AndConstraint::abc(
1187			vec![ShiftedValueIndex::new(
1188				ValueIndex::constant(0),
1189				[Shift::rotr(4), Shift::rotr(5)],
1190			)],
1191			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1192			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1193		));
1194
1195		match cs.validate().unwrap_err() {
1196			ConstraintSystemError::ConstraintOperand {
1197				constraint_kind,
1198				constraint_index,
1199				operand_name,
1200				source: OperandFault::CollapsibleShiftSequence { composition },
1201			} => {
1202				assert_eq!(constraint_kind, ConstraintKind::And);
1203				assert_eq!(constraint_index, 0);
1204				assert_eq!(operand_name, "a");
1205				assert_eq!(composition, Composition::Single(Shift::rotr(9)));
1206			}
1207			other => panic!("Expected CollapsibleShiftSequence, got: {:?}", other),
1208		}
1209	}
1210
1211	#[test]
1212	fn test_validate_rejects_a_pair_that_clears_the_word() {
1213		let mut cs = test_shape();
1214
1215		// Shifting left 40 then left 30 carries every bit past the end, so the term is identically
1216		// zero. The frontend should have deleted it rather than encoded a term that contributes
1217		// nothing.
1218		cs.and_constraints.push(AndConstraint::abc(
1219			vec![ShiftedValueIndex::new(
1220				ValueIndex::constant(0),
1221				[Shift::sll(40), Shift::sll(30)],
1222			)],
1223			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1224			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1225		));
1226
1227		match cs.validate().unwrap_err() {
1228			ConstraintSystemError::ConstraintOperand {
1229				constraint_kind,
1230				constraint_index,
1231				operand_name,
1232				source: OperandFault::CollapsibleShiftSequence { composition },
1233			} => {
1234				assert_eq!(constraint_kind, ConstraintKind::And);
1235				assert_eq!(constraint_index, 0);
1236				assert_eq!(operand_name, "a");
1237				assert_eq!(composition, Composition::Zero);
1238			}
1239			other => panic!("Expected CollapsibleShiftSequence, got: {:?}", other),
1240		}
1241	}
1242
1243	#[test]
1244	fn test_validate_accepts_a_genuine_shift_pair() {
1245		let mut cs = test_shape();
1246
1247		// Clearing the low bits and returning the rest is the canonical irreducible pair: no single
1248		// shift both drops bits and leaves the others where they started.
1249		cs.and_constraints.push(AndConstraint::abc(
1250			vec![ShiftedValueIndex::new(
1251				ValueIndex::constant(0),
1252				[Shift::srl(3), Shift::sll(3)],
1253			)],
1254			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1255			vec![ShiftedValueIndex::plain(ValueIndex::private(0))],
1256		));
1257
1258		cs.validate().unwrap();
1259	}
1260
1261	#[test]
1262	fn test_roundtrip_cs_and_witnesses_reconstruct_valuevec() {
1263		let cs = test_shape();
1264
1265		// Build a value vector and fill every per-instance word with a deterministic pattern. The
1266		// constants come from the system, so they are not supplied here.
1267		let inout = (0..cs.n_inout)
1268			.map(|i| Word::from_u64(0xA5A5_5A5A ^ (i as u64 * 0x9E37_79B9)))
1269			.collect::<Vec<_>>();
1270		let private = (0..cs.n_private)
1271			.map(|i| Word::from_u64(0x5A5A_A5A5 ^ (i as u64 * 0x9E37_79B9)))
1272			.collect::<Vec<_>>();
1273		let values = cs.value_vec_from_data(&inout, &private);
1274
1275		// Serialize only what varies per instance, alongside the system itself
1276		let mut buf_cs = Vec::new();
1277		cs.serialize(&mut buf_cs).unwrap();
1278
1279		let mut buf_inout = Vec::new();
1280		ValuesRef::new(values.inout())
1281			.serialize(&mut buf_inout)
1282			.unwrap();
1283
1284		let mut buf_non_pub = Vec::new();
1285		ValuesRef::new(values.non_public())
1286			.serialize(&mut buf_non_pub)
1287			.unwrap();
1288
1289		// Deserialize everything back
1290		let cs2 = ConstraintSystem::deserialize(&mut buf_cs.as_slice()).unwrap();
1291		let inout2 = ValuesData::deserialize(&mut buf_inout.as_slice()).unwrap();
1292		let non_pub2 = ValuesData::deserialize(&mut buf_non_pub.as_slice()).unwrap();
1293		assert_eq!(cs2.n_inout, inout2.len());
1294		assert_eq!(cs2.n_private, non_pub2.len());
1295
1296		// Reconstruct ValueVec from deserialized pieces
1297		let reconstructed = cs2.value_vec_from_data(&inout2, &non_pub2);
1298
1299		assert_eq!(reconstructed.combined_witness(), values.combined_witness());
1300	}
1301
1302	/// A system whose only constraints are `n` zero constraints, each reading one hidden word.
1303	///
1304	///     [ _ _ _ _ _ _ _ _ ][ v_0 .. v_(n-1) ... ]
1305	///       0 ...        7     8 ...
1306	fn zero_constraint_system(n: usize) -> ConstraintSystem {
1307		ConstraintSystem {
1308			constants: vec![],
1309			n_inout: 0,
1310			n_private: 8,
1311			zero_constraints: (0..n)
1312				.map(|i| ZeroConstraint::plain([ValueIndex::private(i as u32)]))
1313				.collect(),
1314			and_constraints: vec![],
1315			imul_constraints: vec![],
1316			bmul_constraints: vec![],
1317		}
1318	}
1319
1320	#[test]
1321	fn verify_accepts_a_value_vector_satisfying_every_constraint() {
1322		let cs = zero_constraint_system(3);
1323		let values = cs.value_vec_from_data(&[Word::ZERO; 8], &[Word::ZERO; 8]);
1324
1325		assert!(cs.verify(&values).is_ok());
1326	}
1327
1328	#[test]
1329	fn verify_reports_the_index_of_the_first_unsatisfied_constraint() {
1330		// Constraints 0 and 2 hold; only constraint 1 reads a nonzero word.
1331		let cs = zero_constraint_system(3);
1332		let mut private = [Word::ZERO; 8];
1333		private[1] = Word::from_u64(0xabc);
1334		let values = cs.value_vec_from_data(&[Word::ZERO; 8], &private);
1335
1336		let err = cs.verify(&values).unwrap_err();
1337
1338		// The message names the kind, the position and the failing arithmetic.
1339		// Printing the error alone is therefore enough to locate the constraint.
1340		assert_eq!(err.to_string(), "zero #1 is unsatisfied: 0000000000000abc != 0");
1341
1342		match err {
1343			VerificationError::Unsatisfied {
1344				constraint_index,
1345				source,
1346			} => {
1347				assert_eq!(constraint_index, 1);
1348				assert_eq!(source.kind(), ConstraintKind::Zero);
1349				match source {
1350					ConstraintViolation::Zero { val } => assert_eq!(val, 0xabc),
1351					other => panic!("wrong violation: {other:?}"),
1352				}
1353			}
1354			other => panic!("wrong error: {other:?}"),
1355		}
1356	}
1357
1358	#[test]
1359	fn verify_rejects_a_value_vector_that_opens_a_constant_to_the_wrong_word() {
1360		// The vector opens the third constant to a different word than the system declares.
1361		// Constraints read constants through the vector, so this is a different system.
1362		let cs = ConstraintSystem {
1363			zero_constraints: vec![],
1364			..test_shape()
1365		};
1366		// `value_vec_from_data` sources the constants from the system, so it cannot open one to
1367		// the wrong word — the vector is built directly to inject the disagreement. A vector the
1368		// circuit filled can still carry one, which is what `verify` guards against.
1369		let mut public = [Word::ZERO; 8];
1370		public[0] = Word::from_u64(1);
1371		public[1] = Word::from_u64(42);
1372		public[2] = Word::from_u64(0xBAADF00D);
1373		let values = ValueVec::new_from_data(cs.n_const(), &public, &[Word::ZERO; 8]);
1374
1375		match cs.verify(&values).unwrap_err() {
1376			VerificationError::ConstantMismatch {
1377				value_index,
1378				expected,
1379				actual,
1380			} => {
1381				assert_eq!(value_index, 2);
1382				assert_eq!(expected, 0xDEADBEEF);
1383				assert_eq!(actual, 0xBAADF00D);
1384			}
1385			other => panic!("wrong error: {other:?}"),
1386		}
1387	}
1388
1389	/// `n_inout` is declared, not derived: it serializes as a plain number with no backing data.
1390	/// `ZKVerifier::setup` allocates a word per inout value, so a payload of a few hundred bytes
1391	/// could otherwise demand gigabytes.
1392	#[test]
1393	fn an_oversized_inout_segment_is_rejected() {
1394		let mut cs = test_shape();
1395		cs.n_inout = ConstraintSystem::MAX_VALUES_PER_SEGMENT + 1;
1396
1397		assert!(matches!(
1398			cs.validate(),
1399			Err(ConstraintSystemError::SegmentTooLarge {
1400				segment: ValueSegment::InOut,
1401				..
1402			})
1403		));
1404	}
1405
1406	#[test]
1407	fn an_oversized_private_segment_is_rejected() {
1408		let mut cs = test_shape();
1409		cs.n_private = ConstraintSystem::MAX_VALUES_PER_SEGMENT + 1;
1410
1411		assert!(matches!(
1412			cs.validate(),
1413			Err(ConstraintSystemError::SegmentTooLarge {
1414				segment: ValueSegment::Private,
1415				..
1416			})
1417		));
1418	}
1419
1420	/// The bound is inclusive, so a system sitting exactly on it still validates. Pinned
1421	/// separately because an off-by-one here would reject the largest legitimate circuit rather
1422	/// than only crafted ones.
1423	#[test]
1424	fn segments_exactly_at_the_cap_are_accepted() {
1425		let mut cs = test_shape();
1426		cs.n_inout = ConstraintSystem::MAX_VALUES_PER_SEGMENT;
1427		cs.n_private = ConstraintSystem::MAX_VALUES_PER_SEGMENT;
1428
1429		assert!(cs.validate().is_ok());
1430	}
1431}