Skip to main content

binius_core/constraint_system/
m4.rs

1// Copyright 2026 The Binius Developers
2
3use std::{borrow::Cow, iter};
4
5use super::{ValueVec, constraint::Operand};
6use crate::{
7	ConstraintSystem, Word,
8	error::{ConstraintSystemError, VerificationM4Error},
9};
10
11mod witness;
12
13pub use witness::WitnessM4;
14
15/// The chip instances of an M4 witness, addressed by chip ID and row.
16///
17/// [`ConstraintSystemM4::verify`] reads one instance at a time and never holds two, so this is all
18/// it needs of a witness. A witness that stores its instances packed — as the prover's tables do,
19/// one column per instance — can therefore serve them one at a time rather than expanding the whole
20/// witness into value vectors first, which for a system of any size is most of its memory.
21///
22/// Instances are served, not lent, because a packed witness has to build one to hand it over. One
23/// that already holds value vectors lends them and pays nothing.
24pub trait ChipInstances {
25	/// The number of chips the witness covers, which must be the number the system has.
26	fn n_chips(&self) -> usize;
27
28	/// The number of instances of the given chip, which must be at least its active-instance count.
29	fn n_instances(&self, chip_id: usize) -> usize;
30
31	/// The value vector of one instance of one chip.
32	fn instance(&self, chip_id: usize, row: usize) -> Cow<'_, ValueVec>;
33}
34
35impl ChipInstances for [Vec<ValueVec>] {
36	fn n_chips(&self) -> usize {
37		self.len()
38	}
39
40	fn n_instances(&self, chip_id: usize) -> usize {
41		self[chip_id].len()
42	}
43
44	fn instance(&self, chip_id: usize, row: usize) -> Cow<'_, ValueVec> {
45		Cow::Borrowed(&self[chip_id][row])
46	}
47}
48
49/// A constraint system that represents a single chip in a [`ConstraintSystemM4`].
50///
51/// The [`crate::constraint_system::ShiftedValueIndex`] values in the constraints and in the chip
52/// calls name words of the embedded constraint system's value vector, each index counting within
53/// its own segment. See [`ConstraintSystem`] for the exact layout.
54///
55/// ## Validity criteria
56/// * every operand of every chip call in `chip_calls` references a value of this chip
57///
58/// The `chip_id` of a chip call names a chip of the enclosing system, which this type does not
59/// know; [`ConstraintSystemM4::validate`] is what range-checks it.
60#[derive(Debug, Clone)]
61pub struct EmbeddedConstraintSystem {
62	/// The constraints one instance of the chip must satisfy.
63	pub cs: ConstraintSystem,
64	/// The chips this one delegates subrelations to, one entry per call per instance.
65	pub chip_calls: Vec<ChipCall>,
66}
67
68impl EmbeddedConstraintSystem {
69	/// Checks the constraint system and the chip-call operands over it.
70	pub fn validate(&self) -> Result<(), ConstraintSystemError> {
71		self.cs.validate()?;
72
73		for (call_index, chip_call) in self.chip_calls.iter().enumerate() {
74			for (operand_index, operand) in chip_call.inout.iter().enumerate() {
75				if let Some(source) = self.cs.operand_fault(operand) {
76					return Err(ConstraintSystemError::ChipCallOperand {
77						call_index,
78						operand_index,
79						source,
80					});
81				}
82			}
83		}
84
85		Ok(())
86	}
87}
88
89/// One invocation of a chip by the constraint system holding this call.
90#[derive(Debug, Clone)]
91pub struct ChipCall {
92	/// The ID of the chip being called: its index in [`ConstraintSystemM4::chips`].
93	pub chip_id: usize,
94	/// The instance of the callee that the caller's first instance invokes.
95	///
96	/// A call site runs once per instance of its caller, so it claims the callee's instances
97	/// `first_instance..first_instance + n_active`, where `n_active` is the *caller's* own
98	/// active-instance count: the caller's instance `i` invokes instance `first_instance + i`.
99	/// Main runs once, so each of its calls claims a single instance.
100	///
101	/// Like the active-instance counts, this is denormalized — the call graph already says it, and
102	/// [`ConstraintSystemM4::validate`] holds the two to each other.
103	pub first_instance: usize,
104	/// The words passed as the callee's inout values, positionally, as operands over the caller's
105	/// value vector.
106	///
107	/// There is at most one operand per inout value of the callee; the values past them are
108	/// constrained to zero.
109	pub inout: Vec<Operand>,
110}
111
112/// An M4 constraint system.
113///
114/// An M4 constraint system is essentially defined by the composition of chips, each of which
115/// validates a relation on its local inout values. Chips can delegate subrelation constraints to
116/// other chips via chip calls.
117///
118/// Validity invariants:
119/// - all embedded constraint systems in `chips` must have a chip_id value that indexes into `chips`
120/// - the chip calls must claim each chip's active instances exactly once between them
121///
122/// The chips are in topological order: a chip calls only chips with a higher ID, and main, which
123/// is not one of them, calls any. Enumerating the chips in ID order therefore reaches every caller
124/// of a chip before the chip itself, which is what lets one pass assign the instances and one pass
125/// generate the witness. [`Self::validate`] requires the ordering, which makes the call graph
126/// acyclic as a consequence.
127pub struct ConstraintSystemM4 {
128	/// The entry point of the system. It calls chips, but no chip ID names it, so nothing calls
129	/// it.
130	pub main: EmbeddedConstraintSystem,
131	/// The chips, indexed by chip ID, each paired with its number of active instances.
132	///
133	/// A chip runs once per call that reaches it, and those instances are the active ones: only
134	/// they have their own chip calls enforced. The instances past them pad the count and are
135	/// matched by no call.
136	pub chips: Vec<(EmbeddedConstraintSystem, usize)>,
137}
138
139impl ConstraintSystemM4 {
140	/// Checks every chip and every chip call, requires the chips to be in topological order, and
141	/// holds the instances the calls name against the ones the call graph gives them.
142	pub fn validate(&self) -> Result<(), ConstraintSystemError> {
143		self.main.validate()?;
144		self.validate_calls(None, &self.main.chip_calls)?;
145
146		for (chip_index, (chip, _)) in self.chips.iter().enumerate() {
147			chip.validate()?;
148			self.validate_calls(Some(chip_index), &chip.chip_calls)?;
149		}
150
151		self.validate_instances()
152	}
153
154	/// Checks that one caller's calls name existing chips, run only to later chips, and pass no
155	/// more operands than the callee has inout values.
156	///
157	/// A call may pass fewer: the inout values past its operands are constrained to zero.
158	///
159	/// Main is not one of the numbered chips and runs before all of them, so it may call any.
160	fn validate_calls(
161		&self,
162		chip_index: Option<usize>,
163		calls: &[ChipCall],
164	) -> Result<(), ConstraintSystemError> {
165		let n_chips = self.chips.len();
166		for (call_index, call) in calls.iter().enumerate() {
167			if call.chip_id >= n_chips {
168				return Err(ConstraintSystemError::OutOfRangeChipId {
169					chip_index,
170					chip_id: call.chip_id,
171					n_chips,
172				});
173			}
174			if let Some(caller) = chip_index
175				&& call.chip_id <= caller
176			{
177				return Err(ConstraintSystemError::CallOutOfOrder {
178					chip_index: caller,
179					callee: call.chip_id,
180				});
181			}
182			let n_inout = self.chips[call.chip_id].0.cs.n_inout;
183			if call.inout.len() > n_inout {
184				return Err(ConstraintSystemError::WrongCallArity {
185					chip_index,
186					call_index,
187					chip_id: call.chip_id,
188					arity: call.inout.len(),
189					n_inout,
190				});
191			}
192		}
193		Ok(())
194	}
195
196	/// Checks that the calls claim each chip's active instances exactly once between them.
197	///
198	/// A call site claims one instance of its callee per instance of its caller, and the claims are
199	/// handed out in the order the chips are populated: main's calls first, then the calls of each
200	/// chip in ID order. Only main and lower-numbered chips call a chip, so one pass in ID order
201	/// settles a chip's own claims before reading them.
202	///
203	/// This runs after [`Self::validate_calls`] has range-checked every `chip_id` and rejected the
204	/// calls that run backwards, both of which it relies on.
205	fn validate_instances(&self) -> Result<(), ConstraintSystemError> {
206		let mut n_claimed = vec![0usize; self.chips.len()];
207		let callers = iter::once((None, &self.main, 1)).chain(
208			self.chips
209				.iter()
210				.enumerate()
211				.map(|(chip_index, (chip, n_active))| (Some(chip_index), chip, *n_active)),
212		);
213		for (chip_index, caller, n_active) in callers {
214			for (call_index, call) in caller.chip_calls.iter().enumerate() {
215				let claimed = &mut n_claimed[call.chip_id];
216				if call.first_instance != *claimed {
217					return Err(ConstraintSystemError::WrongCallInstance {
218						chip_index,
219						call_index,
220						first_instance: call.first_instance,
221						expected: *claimed,
222					});
223				}
224
225				// The claims multiply down the call graph — a chain whose every chip calls the
226				// next twice reaches `2^depth` — so a system of a few dozen chips can outgrow a
227				// `usize`. Counting it out unchecked would wrap to a total that a declared count
228				// could then agree with.
229				*claimed = claimed.checked_add(n_active).ok_or(
230					ConstraintSystemError::TooManyInstances {
231						chip_id: call.chip_id,
232					},
233				)?;
234			}
235		}
236
237		for (chip_id, ((_, n_active), &claimed)) in iter::zip(&self.chips, &n_claimed).enumerate() {
238			if claimed != *n_active {
239				return Err(ConstraintSystemError::WrongActiveInstanceCount {
240					chip_id,
241					declared: *n_active,
242					actual: claimed,
243				});
244			}
245		}
246
247		Ok(())
248	}
249
250	/// Checks that a witness satisfies this system.
251	///
252	/// The witness is the main chip's value vector and, per chip, one value vector per instance.
253	/// It must satisfy:
254	///
255	/// - the main chip's constraints, on `main`;
256	/// - each chip's constraints, on every one of its instances — the instances past the active
257	///   ones included, since every instance is committed;
258	/// - every chip call, by the instance it names: the caller's instance `i` invokes the callee's
259	///   instance [`first_instance`](ChipCall::first_instance) plus `i`, and passes exactly the
260	///   inout words that instance holds. An inout value the call has no operand for must hold
261	///   zero.
262	///
263	/// This is the reference the proving protocol's argument is checked against, in the manner of
264	/// [`ConstraintSystem::verify`].
265	///
266	/// Instances are read through [`ChipInstances`] one at a time, so a witness that stores them
267	/// packed is never expanded whole. The price is that an instance serving a call is built again
268	/// when the call is checked, having already been built for its own constraints.
269	///
270	/// Malformed systems are [`Self::validate`]'s to reject; verifying one may panic, misreport, or
271	/// pass. In particular a call passing more operands than the callee has inout values — which
272	/// [`Self::validate`] rejects — has the operands past them ignored here.
273	///
274	/// # Errors
275	///
276	/// Reports the first failure found, in the order listed above.
277	pub fn verify<I: ChipInstances + ?Sized>(
278		&self,
279		main: &ValueVec,
280		chip_instances: &I,
281	) -> Result<(), VerificationM4Error> {
282		if chip_instances.n_chips() != self.chips.len() {
283			return Err(VerificationM4Error::WrongChipCount {
284				n_witness_chips: chip_instances.n_chips(),
285				n_chips: self.chips.len(),
286			});
287		}
288
289		self.main.cs.verify(main)?;
290		for (chip_id, (chip, n_active)) in self.chips.iter().enumerate() {
291			let n_instances = chip_instances.n_instances(chip_id);
292			if n_instances < *n_active {
293				return Err(VerificationM4Error::MissingInstances {
294					chip_id,
295					n_instances,
296					n_active: *n_active,
297				});
298			}
299			for instance in 0..n_instances {
300				chip.cs
301					.verify(&chip_instances.instance(chip_id, instance))
302					.map_err(|source| VerificationM4Error::ChipInstance {
303						chip_id,
304						instance,
305						source,
306					})?;
307			}
308		}
309
310		self.check_caller(None, &self.main.chip_calls, main, chip_instances)?;
311		for (caller_chip, (chip, n_active)) in self.chips.iter().enumerate() {
312			for caller_instance in 0..*n_active {
313				let values = chip_instances.instance(caller_chip, caller_instance);
314				self.check_caller(
315					Some((caller_chip, caller_instance)),
316					&chip.chip_calls,
317					&values,
318					chip_instances,
319				)?;
320			}
321		}
322
323		Ok(())
324	}
325
326	/// Checks one caller's calls against the instances they name.
327	///
328	/// `caller` names the caller for diagnostics: a chip instance, or `None` for the main chip.
329	/// `values` is that caller's value vector, which the call operands are evaluated on.
330	///
331	/// The instance a call reaches is its own, offset by which instance of the caller is making it.
332	/// That every one of them exists is [`Self::validate`]'s to establish.
333	fn check_caller<I: ChipInstances + ?Sized>(
334		&self,
335		caller: Option<(usize, usize)>,
336		calls: &[ChipCall],
337		values: &ValueVec,
338		chip_instances: &I,
339	) -> Result<(), VerificationM4Error> {
340		// Main runs once, so its calls are the ones its single instance makes.
341		let caller_instance = caller.map_or(0, |(_, instance)| instance);
342		for (call_index, call) in calls.iter().enumerate() {
343			let chip_id = call.chip_id;
344			let chip = &self.chips[chip_id].0;
345			let row = call.first_instance + caller_instance;
346
347			// Only the callee's own inout values are compared. An operand past them is one
348			// `validate` rejects, and nothing here would have a word to compare it to.
349			let n_inout = chip.cs.n_inout;
350			let instance = chip_instances.instance(chip_id, row);
351			let served = instance.inout();
352			for word in 0..n_inout {
353				let passed = call
354					.inout
355					.get(word)
356					.map(|operand| values.eval_operand(operand))
357					.unwrap_or(Word::ZERO);
358				if served[word] != passed {
359					return Err(VerificationM4Error::CallMismatch {
360						chip_id,
361						row,
362						caller,
363						call_index,
364						word,
365						passed: passed.as_u64(),
366						served: served[word].as_u64(),
367					});
368				}
369			}
370		}
371		Ok(())
372	}
373}
374
375#[cfg(test)]
376mod tests {
377	use super::{
378		super::{ShiftedValueIndex, ValueIndex, ValueSegment, ValueVecLayout, ZeroConstraint},
379		*,
380	};
381	use crate::error::{OperandFault, VerificationError};
382
383	/// The value-vector layout every chip in these tests is shaped by.
384	const LAYOUT: ValueVecLayout = ValueVecLayout {
385		n_const: 0,
386		n_inout: 8,
387		n_witness: 8,
388		n_internal: 0,
389		n_scratch: 0,
390	};
391
392	/// A chip that calls each of `callees` once, over a value vector of 8 inout and 8 private
393	/// words.
394	///
395	/// The calls name instance 0 until [`system`] gives them the ones the call graph does.
396	fn chip(callees: &[usize]) -> EmbeddedConstraintSystem {
397		let cs = LAYOUT.constraint_system_shape(vec![]);
398		let chip_calls = callees
399			.iter()
400			.map(|&chip_id| ChipCall {
401				chip_id,
402				first_instance: 0,
403				inout: vec![],
404			})
405			.collect();
406		EmbeddedConstraintSystem { cs, chip_calls }
407	}
408
409	/// An all-zero instance of a chip, which satisfies one with no constraints of its own.
410	fn instance() -> ValueVec {
411		ValueVec::new(&LAYOUT)
412	}
413
414	/// A system whose main chip calls each of `main_callees` once, over the given chips, with the
415	/// instances and active-instance counts the call graph gives them.
416	///
417	/// This is the assignment `CircuitM4::recompute_instances` writes in the frontend, redone here
418	/// because these systems are built by hand rather than lowered from a circuit. Like it, this
419	/// panics on a call to a chip the system does not have, so a test wanting one corrupts a
420	/// system built without it.
421	fn system(main_callees: &[usize], chips: Vec<EmbeddedConstraintSystem>) -> ConstraintSystemM4 {
422		let mut cs = ConstraintSystemM4 {
423			main: chip(main_callees),
424			chips: chips.into_iter().map(|chip| (chip, 0)).collect(),
425		};
426
427		let mut n_calls = vec![0usize; cs.chips.len()];
428		for call in &mut cs.main.chip_calls {
429			call.first_instance = n_calls[call.chip_id];
430			n_calls[call.chip_id] += 1;
431		}
432		for chip_index in 0..cs.chips.len() {
433			let n_active = n_calls[chip_index];
434			cs.chips[chip_index].1 = n_active;
435			for call in &mut cs.chips[chip_index].0.chip_calls {
436				call.first_instance = n_calls[call.chip_id];
437				n_calls[call.chip_id] = n_calls[call.chip_id].saturating_add(n_active);
438			}
439		}
440		cs
441	}
442
443	#[test]
444	fn validate_rejects_a_chip_call_operand_past_its_own_segment() {
445		// Inout index 8 is the word after the chip's eight inout values. Its position in the value
446		// vector is one a private value occupies, so only a per-segment check catches it.
447		let mut cs = system(&[0], vec![chip(&[])]);
448		cs.main.chip_calls[0].inout = vec![vec![ShiftedValueIndex::plain(ValueIndex::inout(8))]];
449		assert!(matches!(
450			cs.validate(),
451			Err(ConstraintSystemError::ChipCallOperand {
452				call_index: 0,
453				operand_index: 0,
454				source: OperandFault::OutOfRangeValueIndex {
455					segment: ValueSegment::InOut,
456					value_index: 8,
457					segment_len: 8,
458				},
459			})
460		));
461	}
462
463	#[test]
464	fn validate_rejects_a_call_passing_more_operands_than_the_callee_takes() {
465		// The chips have eight inout values; nine operands leave one with no value to land in.
466		let mut cs = system(&[0], vec![chip(&[])]);
467		cs.main.chip_calls[0].inout = vec![vec![]; 9];
468		assert!(matches!(
469			cs.validate(),
470			Err(ConstraintSystemError::WrongCallArity {
471				chip_index: None,
472				call_index: 0,
473				chip_id: 0,
474				arity: 9,
475				n_inout: 8,
476			})
477		));
478	}
479
480	#[test]
481	fn validate_accepts_calls_running_only_to_later_chips() {
482		// Main calls 0, which calls 1 and 2; 1 calls 2, and 2 is a leaf. Main is not one of the
483		// numbered chips, so its call to 0 is not a backward one.
484		let cs = system(&[0], vec![chip(&[1, 2]), chip(&[2]), chip(&[])]);
485		cs.validate().unwrap();
486	}
487
488	#[test]
489	fn validate_rejects_a_chip_that_calls_itself() {
490		// A chip is not later than itself, so self-recursion is out of order rather than a
491		// separate case.
492		let cs = system(&[0], vec![chip(&[0])]);
493		assert!(matches!(
494			cs.validate(),
495			Err(ConstraintSystemError::CallOutOfOrder {
496				chip_index: 0,
497				callee: 0,
498			})
499		));
500	}
501
502	#[test]
503	fn validate_rejects_a_chip_that_calls_an_earlier_chip() {
504		// Chip 2 calls back to chip 1, which is populated before it. The graph as a whole need not
505		// be cyclic for this to break the one pass over the chips.
506		let cs = system(&[0], vec![chip(&[1]), chip(&[2]), chip(&[1])]);
507		assert!(matches!(
508			cs.validate(),
509			Err(ConstraintSystemError::CallOutOfOrder {
510				chip_index: 2,
511				callee: 1,
512			})
513		));
514	}
515
516	#[test]
517	fn validate_rejects_a_call_to_a_chip_that_does_not_exist() {
518		// Chip 0's first call is to a later chip, so the out-of-range second one is the only
519		// fault and the range check is what has to catch it.
520		let mut cs = system(&[0], vec![chip(&[1]), chip(&[])]);
521		cs.chips[0].0.chip_calls.push(ChipCall {
522			chip_id: 7,
523			first_instance: 0,
524			inout: vec![],
525		});
526		assert!(matches!(
527			cs.validate(),
528			Err(ConstraintSystemError::OutOfRangeChipId {
529				chip_index: Some(0),
530				chip_id: 7,
531				n_chips: 2,
532			})
533		));
534	}
535
536	#[test]
537	fn validate_rejects_a_main_call_to_a_chip_that_does_not_exist() {
538		let mut cs = system(&[0], vec![chip(&[])]);
539		cs.main.chip_calls[0].chip_id = 7;
540		assert!(matches!(
541			cs.validate(),
542			Err(ConstraintSystemError::OutOfRangeChipId {
543				chip_index: None,
544				chip_id: 7,
545				n_chips: 1,
546			})
547		));
548	}
549
550	// A call site claims one instance of its callee per instance of its caller, so the instance a
551	// call names is the one the calls before it leave free.
552	#[test]
553	fn validate_rejects_a_call_naming_the_wrong_instance() {
554		let mut cs = system(&[0, 0], vec![chip(&[])]);
555		cs.main.chip_calls[1].first_instance = 0;
556		assert!(matches!(
557			cs.validate(),
558			Err(ConstraintSystemError::WrongCallInstance {
559				chip_index: None,
560				call_index: 1,
561				first_instance: 0,
562				expected: 1,
563			})
564		));
565	}
566
567	// The call-to-instance map is a bijection, not just an injection: a chip claimed by more
568	// invocations than it has active instances has calls that no row answers for.
569	#[test]
570	fn validate_rejects_more_invocations_than_a_chip_has_active_instances() {
571		// Main calls chip 0 twice; declaring one active instance leaves the second call unanswered.
572		let mut cs = system(&[0, 0], vec![chip(&[])]);
573		cs.chips[0].1 = 1;
574		assert!(matches!(
575			cs.validate(),
576			Err(ConstraintSystemError::WrongActiveInstanceCount {
577				chip_id: 0,
578				declared: 1,
579				actual: 2,
580			})
581		));
582	}
583
584	// Instance counts multiply down the call graph, so a chain of a few dozen chips outgrows a
585	// `usize`. Counting it out unchecked would wrap to a plausible-looking total.
586	#[test]
587	fn validate_rejects_an_instance_count_that_outgrows_a_usize() {
588		// A chain of 70 chips, each calling the next twice, so chip `i` is reached 2^i times.
589		const DEPTH: usize = 70;
590		let chips = (0..DEPTH)
591			.map(|i| {
592				let callees = if i + 1 < DEPTH {
593					vec![i + 1, i + 1]
594				} else {
595					vec![]
596				};
597				chip(&callees)
598			})
599			.collect();
600
601		// Chip 63 is reached 2^63 times and calls chip 64 twice, which is where the count leaves
602		// the range.
603		let cs = system(&[0], chips);
604		assert!(matches!(
605			cs.validate(),
606			Err(ConstraintSystemError::TooManyInstances { chip_id: 64 })
607		));
608	}
609
610	// A call passing fewer operands than the callee has inout values constrains the rest to zero,
611	// so an all-zero instance serves a call that passes nothing at all.
612	#[test]
613	fn verify_constrains_the_inout_values_a_call_passes_no_operand_for() {
614		let cs = system(&[0], vec![chip(&[])]);
615		let main = instance();
616		cs.verify(&main, &[vec![instance()]][..]).unwrap();
617
618		// Give the instance a nonzero word where the call passes nothing, and the zero-fill is what
619		// it stops matching.
620		let mut served = instance();
621		served[ValueIndex::inout(3)] = Word::from_u64(0xdead);
622		let err = cs.verify(&main, &[vec![served]][..]).unwrap_err();
623		assert!(
624			matches!(
625				err,
626				VerificationM4Error::CallMismatch {
627					chip_id: 0,
628					row: 0,
629					caller: None,
630					word: 3,
631					passed: 0,
632					served: 0xdead,
633					..
634				}
635			),
636			"{err}"
637		);
638	}
639
640	#[test]
641	fn verify_rejects_a_witness_covering_the_wrong_number_of_chips() {
642		let cs = system(&[0], vec![chip(&[])]);
643		let err = cs.verify(&instance(), &[][..]).unwrap_err();
644		assert!(
645			matches!(
646				err,
647				VerificationM4Error::WrongChipCount {
648					n_witness_chips: 0,
649					n_chips: 1,
650				}
651			),
652			"{err}"
653		);
654	}
655
656	#[test]
657	fn verify_rejects_a_chip_with_fewer_instances_than_active_ones() {
658		let cs = system(&[0], vec![chip(&[])]);
659		let err = cs.verify(&instance(), &[vec![]][..]).unwrap_err();
660		assert!(
661			matches!(
662				err,
663				VerificationM4Error::MissingInstances {
664					chip_id: 0,
665					n_instances: 0,
666					n_active: 1,
667				}
668			),
669			"{err}"
670		);
671	}
672
673	#[test]
674	fn verify_reports_the_instance_whose_own_constraints_fail() {
675		let mut cs = system(&[0], vec![chip(&[])]);
676		// The chip requires its first inout value to vanish, and the padding instance holds a word
677		// that does not. Every instance is committed, so padding is checked like the rest.
678		cs.chips[0]
679			.0
680			.cs
681			.zero_constraints
682			.push(ZeroConstraint::plain([ValueIndex::inout(0)]));
683
684		let mut padding = instance();
685		padding[ValueIndex::inout(0)] = Word::ONE;
686		let err = cs
687			.verify(&instance(), &[vec![instance(), padding]][..])
688			.unwrap_err();
689		assert!(
690			matches!(
691				err,
692				VerificationM4Error::ChipInstance {
693					chip_id: 0,
694					instance: 1,
695					source: VerificationError::Unsatisfied { .. },
696				}
697			),
698			"{err}"
699		);
700	}
701}