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