Skip to main content

binius_core/constraint_system/
constraint.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3use std::fmt;
4
5use binius_field::BinaryField128bGhash as B128;
6use binius_utils::serialization::{DeserializeBytes, SerializationError, SerializeBytes};
7use bytes::{Buf, BufMut};
8
9use super::{ShiftedValueIndex, ValueIndex, ValueVec};
10use crate::{error::ConstraintViolation, word::Word};
11
12/// Operand type.
13///
14/// An operand in Binius64 is a vector of shifted values. Each item in the vector represents a
15/// term in a XOR combination of shifted values.
16///
17/// To give a couple examples:
18///
19/// ```ignore
20/// vec![] == 0
21/// vec![1] == 1
22/// vec![1, 1] == 1 ^ 1
23/// vec![x >> 5, y << 5] = (x >> 5) ^ (y << 5)
24/// ```
25pub type Operand = Vec<ShiftedValueIndex>;
26
27/// The kind of a constraint of a [`ConstraintSystem`](super::ConstraintSystem).
28///
29/// Each variant identifies one of the constraint types the system holds, and its [`fmt::Display`]
30/// impl gives the lowercase name used in diagnostics.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ConstraintKind {
33	/// A [`ZeroConstraint`].
34	Zero,
35	/// An [`AndConstraint`].
36	And,
37	/// An [`ImulConstraint`].
38	Imul,
39	/// A [`BmulConstraint`].
40	Bmul,
41}
42
43impl fmt::Display for ConstraintKind {
44	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45		let name = match self {
46			Self::Zero => "zero",
47			Self::And => "and",
48			Self::Imul => "imul",
49			Self::Bmul => "bmul",
50		};
51		f.write_str(name)
52	}
53}
54
55/// Zero constraint: `VAL = 0`.
56///
57/// This constraint verifies that a single operand vanishes, where the operand is the XOR of
58/// multiple shifted values from the value vector. It expresses an arbitrary `F_2`-linear relation
59/// among shifted words: a two-term operand constrains two shifted values to be equal, and a
60/// three-term operand `[x, y, z]` forces `z = x ^ y`.
61///
62/// The operands are stored in the order given by [`ZeroConstraint::OPERAND_NAMES`].
63#[derive(Debug, Clone, Default)]
64pub struct ZeroConstraint(pub [Operand; ZeroConstraint::ARITY]);
65
66impl ZeroConstraint {
67	/// Number of operands.
68	pub const ARITY: usize = 1;
69	/// Kind of this constraint.
70	pub const KIND: ConstraintKind = ConstraintKind::Zero;
71	/// Names of the operands, in storage order.
72	pub const OPERAND_NAMES: [&'static str; Self::ARITY] = ["val"];
73
74	/// Creates a new Zero constraint from an XOR combination of the given unshifted values.
75	pub fn plain(val: impl IntoIterator<Item = ValueIndex>) -> ZeroConstraint {
76		ZeroConstraint::new(val.into_iter().map(ShiftedValueIndex::plain))
77	}
78
79	/// Creates a new Zero constraint from an XOR combination of the given shifted values.
80	pub fn new(val: impl IntoIterator<Item = ShiftedValueIndex>) -> ZeroConstraint {
81		ZeroConstraint([val.into_iter().collect()])
82	}
83
84	/// Operand VAL.
85	pub const fn val(&self) -> &Operand {
86		&self.0[0]
87	}
88
89	/// Checks that the given value vector makes the single operand vanish.
90	///
91	/// # Errors
92	///
93	/// Returns the word the operand evaluates to, when that word is nonzero.
94	pub fn verify(&self, values: &ValueVec) -> Result<(), ConstraintViolation> {
95		let Word(val) = values.eval_operand(self.val());
96
97		if val != 0 {
98			return Err(ConstraintViolation::Zero { val });
99		}
100		Ok(())
101	}
102}
103
104impl AsRef<[Operand; ZeroConstraint::ARITY]> for ZeroConstraint {
105	fn as_ref(&self) -> &[Operand; Self::ARITY] {
106		&self.0
107	}
108}
109
110impl SerializeBytes for ZeroConstraint {
111	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
112		self.0.serialize(write_buf)
113	}
114}
115
116impl DeserializeBytes for ZeroConstraint {
117	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
118	where
119		Self: Sized,
120	{
121		<[Operand; Self::ARITY]>::deserialize(read_buf).map(Self)
122	}
123}
124
125/// AND constraint: `A & B = C`.
126///
127/// This constraint verifies that the bitwise AND of operands A and B equals operand C.
128/// Each operand is computed as the XOR of multiple shifted values from the value vector.
129///
130/// The operands are stored in the order given by [`AndConstraint::OPERAND_NAMES`].
131#[derive(Debug, Clone, Default)]
132pub struct AndConstraint(pub [Operand; AndConstraint::ARITY]);
133
134impl AndConstraint {
135	/// Number of operands.
136	pub const ARITY: usize = 3;
137	/// Kind of this constraint.
138	pub const KIND: ConstraintKind = ConstraintKind::And;
139	/// Names of the operands, in storage order.
140	pub const OPERAND_NAMES: [&'static str; Self::ARITY] = ["a", "b", "c"];
141
142	/// Creates a new AND constraint from XOR combinations of the given unshifted values.
143	pub fn plain_abc(
144		a: impl IntoIterator<Item = ValueIndex>,
145		b: impl IntoIterator<Item = ValueIndex>,
146		c: impl IntoIterator<Item = ValueIndex>,
147	) -> AndConstraint {
148		AndConstraint::abc(
149			a.into_iter().map(ShiftedValueIndex::plain),
150			b.into_iter().map(ShiftedValueIndex::plain),
151			c.into_iter().map(ShiftedValueIndex::plain),
152		)
153	}
154
155	/// Creates a new AND constraint from XOR combinations of the given shifted values.
156	pub fn abc(
157		a: impl IntoIterator<Item = ShiftedValueIndex>,
158		b: impl IntoIterator<Item = ShiftedValueIndex>,
159		c: impl IntoIterator<Item = ShiftedValueIndex>,
160	) -> AndConstraint {
161		AndConstraint([
162			a.into_iter().collect(),
163			b.into_iter().collect(),
164			c.into_iter().collect(),
165		])
166	}
167
168	/// Operand A.
169	pub const fn a(&self) -> &Operand {
170		&self.0[0]
171	}
172
173	/// Operand B.
174	pub const fn b(&self) -> &Operand {
175		&self.0[1]
176	}
177
178	/// Operand C.
179	pub const fn c(&self) -> &Operand {
180		&self.0[2]
181	}
182
183	/// Checks that the conjunction of operands A and B equals operand C.
184	///
185	/// # Errors
186	///
187	/// Returns the three evaluated operands, together with the bits on which they disagree.
188	pub fn verify(&self, values: &ValueVec) -> Result<(), ConstraintViolation> {
189		let Word(a) = values.eval_operand(self.a());
190		let Word(b) = values.eval_operand(self.b());
191		let Word(c) = values.eval_operand(self.c());
192
193		let residue = (a & b) ^ c;
194		if residue != 0 {
195			return Err(ConstraintViolation::And { a, b, c, residue });
196		}
197		Ok(())
198	}
199}
200
201impl AsRef<[Operand; AndConstraint::ARITY]> for AndConstraint {
202	fn as_ref(&self) -> &[Operand; Self::ARITY] {
203		&self.0
204	}
205}
206
207impl SerializeBytes for AndConstraint {
208	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
209		self.0.serialize(write_buf)
210	}
211}
212
213impl DeserializeBytes for AndConstraint {
214	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
215	where
216		Self: Sized,
217	{
218		<[Operand; Self::ARITY]>::deserialize(read_buf).map(Self)
219	}
220}
221
222/// IMUL constraint: `A * B = (HI << 64) | LO`.
223///
224/// 64-bit unsigned integer multiplication producing 128-bit result split into high and low 64-bit
225/// words.
226///
227/// The operands are stored in the order given by [`ImulConstraint::OPERAND_NAMES`].
228#[derive(Debug, Clone, Default)]
229pub struct ImulConstraint(pub [Operand; ImulConstraint::ARITY]);
230
231impl ImulConstraint {
232	/// Number of operands.
233	pub const ARITY: usize = 4;
234	/// Kind of this constraint.
235	pub const KIND: ConstraintKind = ConstraintKind::Imul;
236	/// Names of the operands, in storage order.
237	pub const OPERAND_NAMES: [&'static str; Self::ARITY] = ["a", "b", "lo", "hi"];
238
239	/// A operand.
240	pub const fn a(&self) -> &Operand {
241		&self.0[0]
242	}
243
244	/// B operand.
245	pub const fn b(&self) -> &Operand {
246		&self.0[1]
247	}
248
249	/// LO operand.
250	///
251	/// The low 64 bits of the result of the multiplication.
252	pub const fn lo(&self) -> &Operand {
253		&self.0[2]
254	}
255
256	/// HI operand.
257	///
258	/// The high 64 bits of the result of the multiplication.
259	pub const fn hi(&self) -> &Operand {
260		&self.0[3]
261	}
262
263	/// Checks that the product of operands A and B equals the HI and LO word pair.
264	///
265	/// The product is formed over the 128-bit integers.
266	/// Nothing is truncated before it is split into the two 64-bit halves.
267	///
268	/// # Errors
269	///
270	/// Returns the evaluated operands, alongside the halves the product actually has.
271	pub fn verify(&self, values: &ValueVec) -> Result<(), ConstraintViolation> {
272		let Word(a) = values.eval_operand(self.a());
273		let Word(b) = values.eval_operand(self.b());
274		let Word(lo) = values.eval_operand(self.lo());
275		let Word(hi) = values.eval_operand(self.hi());
276
277		let product = a as u128 * b as u128;
278		let expected_lo = product as u64;
279		let expected_hi = (product >> 64) as u64;
280
281		if lo != expected_lo || hi != expected_hi {
282			return Err(ConstraintViolation::Imul {
283				a,
284				b,
285				lo,
286				hi,
287				expected_lo,
288				expected_hi,
289			});
290		}
291		Ok(())
292	}
293}
294
295impl AsRef<[Operand; ImulConstraint::ARITY]> for ImulConstraint {
296	fn as_ref(&self) -> &[Operand; Self::ARITY] {
297		&self.0
298	}
299}
300
301impl SerializeBytes for ImulConstraint {
302	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
303		self.0.serialize(write_buf)
304	}
305}
306
307impl DeserializeBytes for ImulConstraint {
308	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
309	where
310		Self: Sized,
311	{
312		<[Operand; Self::ARITY]>::deserialize(read_buf).map(Self)
313	}
314}
315
316/// BMUL constraint: `A * B = C` in the GHASH field `GF(2^128)`.
317///
318/// Multiplication of two GHASH binary-field elements. Because a field element spans 128 bits while
319/// a word holds only 64, each operand is carried by a pair of words: the `lo` word supplies the low
320/// 64 coefficients (of `1, X, ..., X^63`) and the `hi` word the high 64 (of `X^64, ..., X^127`).
321///
322/// The operands are stored in the order given by [`BmulConstraint::OPERAND_NAMES`].
323#[derive(Debug, Clone, Default)]
324pub struct BmulConstraint(pub [Operand; BmulConstraint::ARITY]);
325
326impl BmulConstraint {
327	/// Number of operands.
328	pub const ARITY: usize = 6;
329	/// Kind of this constraint.
330	pub const KIND: ConstraintKind = ConstraintKind::Bmul;
331	/// Names of the operands, in storage order.
332	pub const OPERAND_NAMES: [&'static str; Self::ARITY] =
333		["a_lo", "a_hi", "b_lo", "b_hi", "c_lo", "c_hi"];
334
335	/// Low word of the A operand.
336	pub const fn a_lo(&self) -> &Operand {
337		&self.0[0]
338	}
339
340	/// High word of the A operand.
341	pub const fn a_hi(&self) -> &Operand {
342		&self.0[1]
343	}
344
345	/// Low word of the B operand.
346	pub const fn b_lo(&self) -> &Operand {
347		&self.0[2]
348	}
349
350	/// High word of the B operand.
351	pub const fn b_hi(&self) -> &Operand {
352		&self.0[3]
353	}
354
355	/// Low word of the C (product) operand.
356	pub const fn c_lo(&self) -> &Operand {
357		&self.0[4]
358	}
359
360	/// High word of the C (product) operand.
361	pub const fn c_hi(&self) -> &Operand {
362		&self.0[5]
363	}
364
365	/// Checks that the product of operands A and B equals operand C in the GHASH field.
366	///
367	/// The multiply runs through the same field implementation the proving system uses.
368	/// This check therefore cannot drift from the arithmetic it mirrors.
369	///
370	/// # Errors
371	///
372	/// Returns the three reassembled elements, alongside the product the factors actually have.
373	pub fn verify(&self, values: &ValueVec) -> Result<(), ConstraintViolation> {
374		let a = eval_element(values, self.a_lo(), self.a_hi());
375		let b = eval_element(values, self.b_lo(), self.b_hi());
376		let c = eval_element(values, self.c_lo(), self.c_hi());
377
378		let expected = u128::from(B128::new(a) * B128::new(b));
379		if c != expected {
380			return Err(ConstraintViolation::Bmul { a, b, c, expected });
381		}
382		Ok(())
383	}
384}
385
386/// Reassembles one GHASH field element from the pair of words carrying it.
387///
388/// A field element spans 128 coefficients while a word holds only 64, so it takes two words:
389///
390/// - bit `i` of the low word is the coefficient of `X^i`.
391/// - bit `i` of the high word is the coefficient of `X^(64 + i)`.
392fn eval_element(values: &ValueVec, lo: &Operand, hi: &Operand) -> u128 {
393	let Word(lo) = values.eval_operand(lo);
394	let Word(hi) = values.eval_operand(hi);
395	lo as u128 | ((hi as u128) << 64)
396}
397
398impl AsRef<[Operand; BmulConstraint::ARITY]> for BmulConstraint {
399	fn as_ref(&self) -> &[Operand; Self::ARITY] {
400		&self.0
401	}
402}
403
404impl SerializeBytes for BmulConstraint {
405	fn serialize(&self, write_buf: impl BufMut) -> Result<(), SerializationError> {
406		self.0.serialize(write_buf)
407	}
408}
409
410impl DeserializeBytes for BmulConstraint {
411	fn deserialize(read_buf: impl Buf) -> Result<Self, SerializationError>
412	where
413		Self: Sized,
414	{
415		<[Operand; Self::ARITY]>::deserialize(read_buf).map(Self)
416	}
417}
418
419#[cfg(test)]
420mod tests {
421	use std::iter;
422
423	use super::*;
424	use crate::constraint_system::ConstraintKind;
425
426	#[test]
427	fn test_zero_constraint_serialization_round_trip() {
428		// One term per segment a constraint may name, so the round trip covers the segment tag as
429		// well as the index.
430		let constraint = ZeroConstraint::new([
431			ShiftedValueIndex::sll(ValueIndex::constant(1), 5),
432			ShiftedValueIndex::srl(ValueIndex::inout(2), 10),
433			ShiftedValueIndex::plain(ValueIndex::private(3)),
434		]);
435
436		let mut buf = Vec::new();
437		constraint.serialize(&mut buf).unwrap();
438
439		let deserialized = ZeroConstraint::deserialize(&mut buf.as_slice()).unwrap();
440		assert_eq!(constraint.val().len(), deserialized.val().len());
441
442		for (orig, deser) in constraint.val().iter().zip(deserialized.val().iter()) {
443			assert_eq!(orig.value_index, deser.value_index);
444			assert_eq!(orig.shift_seq, deser.shift_seq);
445		}
446	}
447
448	#[test]
449	fn test_and_constraint_serialization_round_trip() {
450		let constraint = AndConstraint::abc(
451			vec![ShiftedValueIndex::sll(ValueIndex::constant(1), 5)],
452			vec![ShiftedValueIndex::srl(ValueIndex::constant(2), 10)],
453			vec![
454				ShiftedValueIndex::sar(ValueIndex::constant(3), 15),
455				ShiftedValueIndex::plain(ValueIndex::constant(4)),
456			],
457		);
458
459		let mut buf = Vec::new();
460		constraint.serialize(&mut buf).unwrap();
461
462		let deserialized = AndConstraint::deserialize(&mut buf.as_slice()).unwrap();
463		assert_eq!(constraint.a().len(), deserialized.a().len());
464		assert_eq!(constraint.b().len(), deserialized.b().len());
465		assert_eq!(constraint.c().len(), deserialized.c().len());
466
467		for (orig, deser) in constraint.a().iter().zip(deserialized.a().iter()) {
468			assert_eq!(orig.value_index, deser.value_index);
469			assert_eq!(orig.shift_seq, deser.shift_seq);
470		}
471	}
472
473	#[test]
474	fn test_imul_constraint_serialization_round_trip() {
475		let constraint = ImulConstraint([
476			vec![ShiftedValueIndex::plain(ValueIndex::constant(0))],
477			vec![ShiftedValueIndex::srl(ValueIndex::constant(1), 32)],
478			vec![ShiftedValueIndex::plain(ValueIndex::constant(2))],
479			vec![ShiftedValueIndex::plain(ValueIndex::constant(3))],
480		]);
481
482		let mut buf = Vec::new();
483		constraint.serialize(&mut buf).unwrap();
484
485		let deserialized = ImulConstraint::deserialize(&mut buf.as_slice()).unwrap();
486		assert_eq!(constraint.a().len(), deserialized.a().len());
487		assert_eq!(constraint.b().len(), deserialized.b().len());
488		assert_eq!(constraint.lo().len(), deserialized.lo().len());
489		assert_eq!(constraint.hi().len(), deserialized.hi().len());
490	}
491
492	#[test]
493	fn test_bmul_constraint_serialization_round_trip() {
494		let constraint = BmulConstraint([
495			vec![ShiftedValueIndex::plain(ValueIndex::constant(0))],
496			vec![ShiftedValueIndex::srl(ValueIndex::constant(1), 32)],
497			vec![ShiftedValueIndex::plain(ValueIndex::constant(2))],
498			vec![ShiftedValueIndex::sll(ValueIndex::constant(3), 5)],
499			vec![ShiftedValueIndex::plain(ValueIndex::constant(4))],
500			vec![
501				ShiftedValueIndex::sar(ValueIndex::constant(5), 15),
502				ShiftedValueIndex::plain(ValueIndex::constant(6)),
503			],
504		]);
505
506		let mut buf = Vec::new();
507		constraint.serialize(&mut buf).unwrap();
508
509		let deserialized = BmulConstraint::deserialize(&mut buf.as_slice()).unwrap();
510		assert_eq!(constraint.a_lo().len(), deserialized.a_lo().len());
511		assert_eq!(constraint.a_hi().len(), deserialized.a_hi().len());
512		assert_eq!(constraint.b_lo().len(), deserialized.b_lo().len());
513		assert_eq!(constraint.b_hi().len(), deserialized.b_hi().len());
514		assert_eq!(constraint.c_lo().len(), deserialized.c_lo().len());
515		assert_eq!(constraint.c_hi().len(), deserialized.c_hi().len());
516	}
517
518	/// A GHASH-field triple satisfying `A * B = C`.
519	///
520	/// The product is hard-coded rather than computed.
521	/// The BMUL cases below therefore do not lean on the same multiply they are checking.
522	const A: u128 = 0x0123456789abcdef_fedcba9876543210;
523	const B: u128 = 0x0f1e2d3c4b5a6978_8796a5b4c3d2e1f0;
524	const A_TIMES_B: u128 = 0x7f2984f784967f5a_7b881bf2b700d768;
525
526	/// Builds a value vector whose public segment opens the given words as constants.
527	///
528	/// Both segments are eight words long, which is the shortest shape the shape check accepts.
529	/// The whole public segment is constants, so the inout section is empty and starts at its end:
530	///
531	///     [ w_0 w_1 ... 0 0 ][ 0 0 0 0 0 0 0 0 ]
532	///       0 1          7    8 ...         15
533	fn values(words: &[u64]) -> ValueVec {
534		let mut public = [Word::ZERO; 8];
535		for (slot, &word) in iter::zip(&mut public, words) {
536			*slot = Word::from_u64(word);
537		}
538		ValueVec::new_from_data(public.len(), &public, &[Word::ZERO; 8])
539	}
540
541	/// An operand reading constant `index` unshifted.
542	fn at(index: u32) -> Operand {
543		vec![ShiftedValueIndex::plain(ValueIndex::constant(index))]
544	}
545
546	/// The two words carrying a GHASH element, low half first.
547	fn split(x: u128) -> [u64; 2] {
548		[x as u64, (x >> 64) as u64]
549	}
550
551	#[test]
552	fn zero_constraint_accepts_operand_whose_terms_cancel() {
553		// Two terms reading equal words XOR to zero.
554		// The operand vanishes even though neither word is zero.
555		let values = values(&[0xfeed_face, 0xfeed_face]);
556		let constraint = ZeroConstraint::plain([ValueIndex::constant(0), ValueIndex::constant(1)]);
557
558		assert!(constraint.verify(&values).is_ok());
559	}
560
561	#[test]
562	fn zero_constraint_rejects_operand_that_survives() {
563		let values = values(&[0xfeed_face, 0x0bad_cafe]);
564		let constraint = ZeroConstraint::plain([ValueIndex::constant(0), ValueIndex::constant(1)]);
565
566		match constraint.verify(&values).unwrap_err() {
567			ConstraintViolation::Zero { val } => assert_eq!(val, 0xfeed_face ^ 0x0bad_cafe),
568			other => panic!("wrong violation: {other:?}"),
569		}
570	}
571
572	#[test]
573	fn and_constraint_accepts_matching_conjunction() {
574		let values = values(&[0b1100, 0b1010, 0b1000]);
575		let constraint = AndConstraint::plain_abc(
576			[ValueIndex::constant(0)],
577			[ValueIndex::constant(1)],
578			[ValueIndex::constant(2)],
579		);
580
581		assert!(constraint.verify(&values).is_ok());
582	}
583
584	#[test]
585	fn and_constraint_rejects_mismatched_conjunction() {
586		// C claims one bit too many: 0b1100 & 0b1010 is 0b1000, not 0b1001.
587		let values = values(&[0b1100, 0b1010, 0b1001]);
588		let constraint = AndConstraint::plain_abc(
589			[ValueIndex::constant(0)],
590			[ValueIndex::constant(1)],
591			[ValueIndex::constant(2)],
592		);
593
594		match constraint.verify(&values).unwrap_err() {
595			ConstraintViolation::And { a, b, c, residue } => {
596				assert_eq!(a, 0b1100);
597				assert_eq!(b, 0b1010);
598				assert_eq!(c, 0b1001);
599				assert_eq!(residue, 0b0001);
600			}
601			other => panic!("wrong violation: {other:?}"),
602		}
603	}
604
605	#[test]
606	fn imul_constraint_accepts_both_halves_of_the_product() {
607		// Both factors exceed 2^32, so the product fills the high word as well as the low one.
608		let a = 0x1234_5678_9abc_def0u64;
609		let b = 0x0fed_cba9_8765_4321u64;
610		let product = a as u128 * b as u128;
611		let values = values(&[a, b, product as u64, (product >> 64) as u64]);
612		let constraint = ImulConstraint([at(0), at(1), at(2), at(3)]);
613
614		assert!(constraint.verify(&values).is_ok());
615	}
616
617	#[test]
618	fn imul_constraint_rejects_a_dropped_high_word() {
619		// A prover keeping only the low half is the failure a truncating check would miss.
620		let a = 0x1234_5678_9abc_def0u64;
621		let b = 0x0fed_cba9_8765_4321u64;
622		let product = a as u128 * b as u128;
623		let values = values(&[a, b, product as u64, 0]);
624		let constraint = ImulConstraint([at(0), at(1), at(2), at(3)]);
625
626		match constraint.verify(&values).unwrap_err() {
627			ConstraintViolation::Imul {
628				a: got_a,
629				b: got_b,
630				lo,
631				hi,
632				expected_lo,
633				expected_hi,
634			} => {
635				assert_eq!(got_a, a);
636				assert_eq!(got_b, b);
637				assert_eq!(lo, product as u64);
638				assert_eq!(hi, 0);
639				assert_eq!(expected_lo, product as u64);
640				assert_eq!(expected_hi, (product >> 64) as u64);
641			}
642			other => panic!("wrong violation: {other:?}"),
643		}
644	}
645
646	#[test]
647	fn bmul_constraint_accepts_the_field_product() {
648		let [a_lo, a_hi] = split(A);
649		let [b_lo, b_hi] = split(B);
650		let [c_lo, c_hi] = split(A_TIMES_B);
651		let values = values(&[a_lo, a_hi, b_lo, b_hi, c_lo, c_hi]);
652		let constraint = BmulConstraint([at(0), at(1), at(2), at(3), at(4), at(5)]);
653
654		assert!(constraint.verify(&values).is_ok());
655	}
656
657	#[test]
658	fn bmul_constraint_rejects_a_product_off_by_one_coefficient() {
659		let [a_lo, a_hi] = split(A);
660		let [b_lo, b_hi] = split(B);
661		let [c_lo, c_hi] = split(A_TIMES_B ^ 1);
662		let values = values(&[a_lo, a_hi, b_lo, b_hi, c_lo, c_hi]);
663		let constraint = BmulConstraint([at(0), at(1), at(2), at(3), at(4), at(5)]);
664
665		match constraint.verify(&values).unwrap_err() {
666			ConstraintViolation::Bmul { a, b, c, expected } => {
667				assert_eq!(a, A);
668				assert_eq!(b, B);
669				assert_eq!(c, A_TIMES_B ^ 1);
670				assert_eq!(expected, A_TIMES_B);
671			}
672			other => panic!("wrong violation: {other:?}"),
673		}
674	}
675
676	#[test]
677	fn violation_reports_the_kind_of_constraint_that_failed() {
678		// The kind is derived from the violation rather than stored beside it.
679		// The two therefore cannot disagree.
680		assert_eq!(ConstraintViolation::Zero { val: 1 }.kind(), ConstraintKind::Zero);
681		assert_eq!(
682			ConstraintViolation::And {
683				a: 1,
684				b: 1,
685				c: 0,
686				residue: 1
687			}
688			.kind(),
689			ConstraintKind::And
690		);
691		assert_eq!(
692			ConstraintViolation::Imul {
693				a: 1,
694				b: 1,
695				lo: 0,
696				hi: 0,
697				expected_lo: 1,
698				expected_hi: 0
699			}
700			.kind(),
701			ConstraintKind::Imul
702		);
703		assert_eq!(
704			ConstraintViolation::Bmul {
705				a: 1,
706				b: 1,
707				c: 0,
708				expected: 1
709			}
710			.kind(),
711			ConstraintKind::Bmul
712		);
713	}
714}