Skip to main content

binius_spartan_verifier/wrapper/
mod.rs

1// Copyright 2026 The Binius Developers
2
3//! Spartan wrapper for symbolically executing IOP verifiers to build constraint systems.
4//!
5//! This crate provides [`IronSpartanBuilderChannel`], an implementation of [`IPVerifierChannel`]
6//! that symbolically executes a verifier and records the computation as an IronSpartan constraint
7//! system via [`ConstraintBuilder`].
8//!
9//! [`IPVerifierChannel`]: binius_ip::channel::IPVerifierChannel
10//! [`ConstraintBuilder`]: binius_spartan_frontend::circuit_builder::ConstraintBuilder
11
12pub mod builder_channel;
13pub mod circuit_elem;
14pub mod gadgets;
15pub mod zk_wrapped_channel;
16
17pub use builder_channel::IronSpartanBuilderChannel;
18pub use zk_wrapped_channel::ZKWrappedVerifierChannel;
19
20#[cfg(test)]
21mod tests {
22	use std::rc::Rc;
23
24	use binius_core::word::Word;
25	use binius_field::{
26		BinaryField1b as B1, ExtensionField, Field, Ghash128b as B128, Random,
27		arithmetic_traits::InvertOrZero, field::FieldOps,
28	};
29	use binius_ip::channel::{IPVerifierChannel, WordIPVerifierChannel};
30	use binius_spartan_frontend::circuit_builder::ConstraintBuilder;
31	use rand::{SeedableRng, rngs::StdRng};
32
33	use super::*;
34	use crate::wrapper::circuit_elem::CircuitElem;
35
36	type BuildElem = CircuitElem<B128, ConstraintBuilder<B128>>;
37
38	/// Helper to create a private-backed `BuildElem` wire from a ConstraintBuilder Rc for tests.
39	///
40	/// Uses a precommit wire (the real source of a private wire in wrapper usage — the OTP key) so
41	/// that arithmetic on it stays private and records constraints. An inout-backed wire would
42	/// instead be elided into a derived wire with no constraint.
43	fn alloc_private_wire(rc: &Rc<std::cell::RefCell<ConstraintBuilder<B128>>>) -> BuildElem {
44		let wire = rc.borrow_mut().alloc_precommit();
45		BuildElem::wire(rc, wire)
46	}
47
48	#[test]
49	fn test_constant_arithmetic() {
50		let a = BuildElem::Constant(B128::new(3));
51		let b = BuildElem::Constant(B128::new(5));
52
53		// Addition in char 2: 3 + 5 = 3 ^ 5 = 6
54		let sum = a.clone() + b.clone();
55		assert!(matches!(sum, BuildElem::Constant(c) if c == B128::new(3) + B128::new(5)));
56
57		// Multiplication
58		let product = a.clone() * b.clone();
59		assert!(matches!(product, BuildElem::Constant(c) if c == B128::new(3) * B128::new(5)));
60
61		// Subtraction equals addition in char 2
62		let diff = a - b;
63		assert!(matches!(diff, BuildElem::Constant(c) if c == B128::new(3) + B128::new(5)));
64	}
65
66	#[test]
67	fn test_constant_identity_shortcuts() {
68		let rc = Rc::new(std::cell::RefCell::new(ConstraintBuilder::<B128>::new()));
69		let elem = alloc_private_wire(&rc);
70
71		// Adding zero returns the wire unchanged.
72		let result = elem.clone() + BuildElem::Constant(B128::ZERO);
73		assert!(matches!(result, BuildElem::Wire { .. }));
74
75		// Multiplying by one returns the wire unchanged.
76		let result = elem.clone() * BuildElem::Constant(B128::ONE);
77		assert!(matches!(result, BuildElem::Wire { .. }));
78
79		// Multiplying by zero returns constant zero.
80		let result = elem * BuildElem::Constant(B128::ZERO);
81		assert!(matches!(result, BuildElem::Constant(c) if c == B128::ZERO));
82	}
83
84	#[test]
85	fn test_wire_addition_creates_constraint() {
86		let rc = Rc::new(std::cell::RefCell::new(ConstraintBuilder::<B128>::new()));
87		let a = alloc_private_wire(&rc);
88		let b = alloc_private_wire(&rc);
89
90		let _sum = a + b;
91		// The addition should produce constraints. After finalization, the zero constraint
92		// from addition becomes a mul constraint (multiplied by one).
93		let (cs, _layout) = Rc::try_unwrap(rc).unwrap().into_inner().build().finalize();
94		assert!(!cs.mul_constraints().is_empty());
95	}
96
97	#[test]
98	fn test_wire_multiplication_creates_constraint() {
99		let rc = Rc::new(std::cell::RefCell::new(ConstraintBuilder::<B128>::new()));
100		let a = alloc_private_wire(&rc);
101		let b = alloc_private_wire(&rc);
102
103		let _product = a * b;
104		let (cs, _layout) = Rc::try_unwrap(rc).unwrap().into_inner().build().finalize();
105		// At least 1 mul constraint from a*b.
106		assert!(!cs.mul_constraints().is_empty());
107	}
108
109	#[test]
110	fn test_invert_creates_constraints() {
111		let rc = Rc::new(std::cell::RefCell::new(ConstraintBuilder::<B128>::new()));
112		let elem = alloc_private_wire(&rc);
113
114		// SAFETY: nothing constrains the wire, so the contract is vacuous here; the test only
115		// counts the constraints the inverse emits.
116		let _inv = unsafe { elem.invert() };
117		let (cs, _layout) = Rc::try_unwrap(rc).unwrap().into_inner().build().finalize();
118		// The inverse creates: a mul constraint (wire * inv) and a zero constraint
119		// (product ^ one), both of which become mul constraints after finalization.
120		assert!(cs.mul_constraints().len() >= 2);
121	}
122
123	#[test]
124	#[should_panic(expected = "the wrapper inverts only values argued non-zero")]
125	fn test_invert_or_zero_is_unimplemented() {
126		let rc = Rc::new(std::cell::RefCell::new(ConstraintBuilder::<B128>::new()));
127		let elem = alloc_private_wire(&rc);
128
129		let _ = elem.invert_or_zero();
130	}
131
132	#[test]
133	fn test_channel_recv_and_sample() {
134		let mut channel = IronSpartanBuilderChannel::<B128>::new();
135
136		let a = channel.recv_one().unwrap();
137		let b = channel.sample();
138		let c = channel.recv_array::<3>().unwrap();
139
140		// All should be Wire variants.
141		assert!(matches!(a, BuildElem::Wire { .. }));
142		assert!(matches!(b, BuildElem::Wire { .. }));
143		for elem in &c {
144			assert!(matches!(elem, BuildElem::Wire { .. }));
145		}
146	}
147
148	/// The packed statement enters the circuit as inout wires, not as constants.
149	///
150	/// The wrapper circuit is built once, against whatever statement the symbolic run was handed,
151	/// and reused for every other one. Packing the words into constants would fix that first
152	/// statement into the circuit, so the words become wires the concrete channels fill in.
153	#[test]
154	fn test_pack_words_allocates_inout_wires() {
155		let mut channel = IronSpartanBuilderChannel::<B128>::new();
156
157		// Two words to a `B128`, so three words span two elements. The zero word is deliberate:
158		// nothing about the packing may turn on the values.
159		let words = [Word::from_u64(7), Word::ZERO, Word::from_u64(9)];
160		let elems = channel.pack_words(&words);
161
162		assert_eq!(elems.len(), 2);
163		assert!(
164			elems
165				.iter()
166				.all(|elem| matches!(elem, BuildElem::Wire { .. }))
167		);
168	}
169
170	#[test]
171	fn test_channel_assert_zero() {
172		let mut channel = IronSpartanBuilderChannel::<B128>::new();
173
174		// Assert zero on a constant zero should succeed.
175		assert!(channel.assert_zero(BuildElem::Constant(B128::ZERO)).is_ok());
176
177		// Assert zero on a nonzero constant should fail.
178		assert!(channel.assert_zero(BuildElem::Constant(B128::ONE)).is_err());
179
180		// Assert zero on a wire should succeed (records constraint).
181		let wire_elem = channel.recv_one().unwrap();
182		assert!(channel.assert_zero(wire_elem).is_ok());
183	}
184
185	#[test]
186	fn test_verify_iop_builds_constraint_system() {
187		use binius_spartan_frontend::{
188			circuit_builder::CircuitBuilder, circuits::powers, compiler::compile,
189		};
190
191		use crate::{
192			IOPVerifier,
193			constraint_system::{BlindingInfo, ConstraintSystemPadded},
194		};
195
196		// Build a power7 circuit: assert that x^7 = y.
197		fn power7_circuit<Builder: CircuitBuilder>(
198			builder: &mut Builder,
199			x_wire: Builder::Wire,
200			y_wire: Builder::Wire,
201		) {
202			let powers_vec = powers(builder, x_wire, 7);
203			let x7 = powers_vec[6];
204			builder.assert_eq(x7, y_wire);
205		}
206
207		let mut constraint_builder = ConstraintBuilder::<B128>::new();
208		let x_wire = constraint_builder.alloc_inout();
209		let y_wire = constraint_builder.alloc_inout();
210		power7_circuit(&mut constraint_builder, x_wire, y_wire);
211		let (cs, _layout) = compile(constraint_builder);
212
213		// Build IOPVerifier directly from the constraint system.
214		let blinding_info = BlindingInfo {
215			n_dummy_wires: 10,
216			n_dummy_constraints: 2,
217		};
218		let cs = ConstraintSystemPadded::new(cs, blinding_info);
219		let iop_verifier = IOPVerifier::new(cs);
220		let cs = iop_verifier.constraint_system();
221		let public_size = 1 << cs.log_public();
222
223		// Create the builder channel and run IOPVerifier::verify symbolically.
224		let mut channel = IronSpartanBuilderChannel::<B128>::new();
225
226		// Use zero-filled public inputs of the correct length.
227		let public = vec![B128::ZERO; public_size];
228		let public_elems = channel.observe_many(&public);
229		// IronSpartanBuilderChannel::Oracle = () and recv_oracle is a no-op, so pass () directly.
230		iop_verifier
231			.verify((), &public_elems, &mut channel)
232			.expect("symbolic verify failed");
233
234		let builder = channel.finish();
235
236		// Extract the constraint system built by the symbolic execution.
237		let (wrapper_cs, _layout) = builder.build().finalize();
238
239		// The symbolic execution should have produced a nontrivial constraint system.
240		assert!(wrapper_cs.n_inout() > 0);
241		assert!(!wrapper_cs.mul_constraints().is_empty());
242	}
243
244	#[test]
245	fn test_square_transpose_constants() {
246		type FSub = B1;
247		let degree = <B128 as ExtensionField<FSub>>::DEGREE;
248		let mut rng = StdRng::seed_from_u64(0);
249
250		// Generate random elements and transpose them both natively and via CircuitElem.
251		let values = (0..degree)
252			.map(|_| B128::random(&mut rng))
253			.collect::<Vec<_>>();
254
255		let mut expected = values.clone();
256		<B128 as ExtensionField<FSub>>::square_transpose(&mut expected);
257
258		let mut elems = values
259			.iter()
260			.map(|&v| BuildElem::Constant(v))
261			.collect::<Vec<_>>();
262		<BuildElem as FieldOps>::square_transpose::<FSub>(&mut elems);
263
264		for (i, (elem, &exp)) in elems.iter().zip(&expected).enumerate() {
265			match elem {
266				CircuitElem::Constant(c) => assert_eq!(*c, exp, "mismatch at index {i}"),
267				CircuitElem::Wire { .. } => {
268					panic!("expected constant after all-constants transpose")
269				}
270			}
271		}
272	}
273
274	#[test]
275	fn test_channel_integration_simple_circuit() {
276		// Build a simple circuit: recv two values, multiply them, assert_zero on the
277		// difference with a third received value (ie. a * b == c).
278		let mut channel = IronSpartanBuilderChannel::<B128>::new();
279		let a = channel.recv_one().unwrap();
280		let b = channel.recv_one().unwrap();
281		let c = channel.recv_one().unwrap();
282		let product = a * b;
283		// In char 2, product - c == product + c.
284		let diff = product + c;
285		channel.assert_zero(diff).unwrap();
286
287		// Finish and extract the constraint system.
288		let builder = channel.finish();
289		let (cs, _layout) = builder.build().finalize();
290
291		// Should have inout wires from recv + private wires from mul and add.
292		assert!(cs.n_inout() >= 3);
293		// Should have at least 1 mul constraint from a*b and 1 from the finalized zero
294		// constraint.
295		assert!(!cs.mul_constraints().is_empty());
296	}
297}