Skip to main content

binius_spartan_verifier/
lib.rs

1// Copyright 2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4//! Spartan-based proof verification for Binius64 constraint systems.
5//!
6//! This crate provides the [`Verifier`] struct for verifying zero-knowledge proofs
7//! generated by `binius_spartan_prover`. It implements the Spartan protocol adapted
8//! for Binius64's constraint system.
9//!
10//! # When to use this crate
11//!
12//! Use this crate when you have a constraint system built with `binius_spartan_frontend`
13//! and need to verify a Spartan-based proof. This is an alternative to the main
14//! `binius_verifier` crate.
15//!
16//! # Key types
17//!
18//! - [`Verifier`] - Main verification interface; call [`Verifier::setup`] with a constraint system,
19//!   then [`Verifier::verify`] with a proof and public inputs
20//! - [`IOPVerifier`] - Core IOP verification logic, independent of the compilation strategy
21//! - [`Error`] - Error type returned when proof verification fails
22//!
23//! # Related crates
24//!
25//! - `binius_spartan_prover` - Proving counterpart
26//! - `binius_spartan_frontend` - Constraint system builder for Spartan
27//! - `binius_verifier` - Alternative verification backend
28
29#![warn(rustdoc::missing_crate_level_docs)]
30
31pub mod config;
32pub mod constraint_system;
33pub mod wiring;
34pub mod wrapper;
35
36use std::{marker::PhantomData, rc::Rc};
37
38use binius_field::{BinaryField, Field, field::FieldOps};
39use binius_hash::HashSuite;
40use binius_iop::{
41	basefold,
42	basefold::compiler::BaseFoldVerifierCompiler,
43	channel::{
44		IOPVerifierChannel, OracleSpec,
45		oracle_setup::{DummyElem, OracleSetupChannel},
46	},
47	fri::{self, MinProofSizeStrategy},
48	merkle_tree::BinaryMerkleTreeScheme,
49};
50use binius_ip::{channel::IPVerifierChannel, mlecheck, sumcheck};
51use binius_math::{multilinear::eq::eq_ind_partial_eval_scalars, univariate::evaluate_univariate};
52use binius_spartan_frontend::constraint_system::{ConstraintSystem, WitnessSegment};
53use binius_transcript::{VerifierTranscript, fiat_shamir::Challenger};
54use binius_utils::{DeserializeBytes, checked_arithmetics::checked_log_2};
55use digest::Output;
56
57use crate::constraint_system::{BlindingInfo, ConstraintSystemPadded};
58
59pub const SECURITY_BITS: usize = 96;
60
61/// Output of the multiplication constraint check verification.
62#[derive(Debug, Clone)]
63pub struct MulcheckOutput<F> {
64	/// Evaluation of operand A at the challenge point.
65	pub a_eval: F,
66	/// Evaluation of operand B at the challenge point.
67	pub b_eval: F,
68	/// Evaluation of operand C at the challenge point.
69	pub c_eval: F,
70	/// Evaluation of the mask polynomial at the challenge point.
71	pub mask_eval: F,
72	/// The challenge point from the sumcheck reduction.
73	pub r_x: Vec<F>,
74}
75
76/// IOP verifier for a particular constraint system.
77///
78/// This struct encapsulates the constraint system, providing the core verification logic
79/// independent of the specific IOP compilation strategy. Most users should use [`Verifier`]
80/// instead, which wraps this with a BaseFold compiler.
81#[derive(Debug, Clone)]
82pub struct IOPVerifier<F: Field> {
83	constraint_system: ConstraintSystemPadded<F>,
84}
85
86/// Struct for verifying instances of a particular constraint system.
87///
88/// The [`Self::setup`] constructor determines public parameters for proving instances of the given
89/// constraint system. Then [`Self::verify`] is called one or more times with individual instances.
90#[derive(Clone)]
91pub struct Verifier<F, H>
92where
93	F: BinaryField,
94	H: HashSuite,
95{
96	iop_verifier: IOPVerifier<F>,
97	/// BaseFold ZK compiler for creating verifier channels.
98	basefold_compiler: BaseFoldVerifierCompiler<F>,
99	/// The verifier creates its Merkle transcript channels with the hash suite `H`.
100	_hash_marker: PhantomData<H>,
101}
102
103impl<F: Field> IOPVerifier<F> {
104	/// Constructs an IOP verifier for a constraint system.
105	pub const fn new(constraint_system: ConstraintSystemPadded<F>) -> Self {
106		Self { constraint_system }
107	}
108
109	pub const fn constraint_system(&self) -> &ConstraintSystemPadded<F> {
110		&self.constraint_system
111	}
112
113	/// Returns the oracle specs for the IOP channel.
114	///
115	/// These describe the oracles (witness and mask) that the prover commits to.
116	///
117	/// The specs are derived by replaying the oracle-receiving sequence against an
118	/// [`OracleSetupChannel`] (which records each `recv_oracle` without doing real verification),
119	/// rather than hardcoding it. IronSpartan proofs are always zero-knowledge. The precommit
120	/// oracle is received by the outer [`Verifier::verify`] before it delegates to
121	/// [`Self::verify`], so it is recorded here first to match that order.
122	pub fn oracle_specs(&self) -> Vec<OracleSpec>
123	where
124		F: BinaryField,
125	{
126		let cs = &self.constraint_system;
127		let mut channel = OracleSetupChannel::new(true);
128		// Record the precommit oracle first (`OracleSetupChannel` implements the channel trait for
129		// every `F`, so the trait is named explicitly to fix `F`); the `verify` call below fixes it
130		// for the rest of the sequence.
131		<OracleSetupChannel as IOPVerifierChannel<F>>::recv_oracle(
132			&mut channel,
133			cs.log_precommit() as usize,
134			true,
135		)
136		.expect("OracleSetupChannel::recv_oracle is infallible");
137		let public = vec![DummyElem::<F>::default(); 1 << cs.log_public()];
138		// Discarded: the setup channel performs no real verification; we only read back the
139		// recorded oracle specs.
140		let _ = self.verify((), &public, &mut channel);
141		channel.into_oracle_specs()
142	}
143
144	/// Verifies a proof using an IOP channel.
145	///
146	/// This is the core verification logic, independent of the specific IOP compilation strategy.
147	/// For most users, [`Verifier::verify`] is the simpler interface.
148	///
149	/// # Arguments
150	///
151	/// * `precommit_oracle` - Handle to the precommit oracle, received from the channel by the
152	///   caller before invoking `verify`.
153	/// * `public` - The public inputs to the constraint system
154	/// * `channel` - The IOP verifier channel
155	///
156	/// # Returns
157	///
158	/// `Ok(())` if the proof is valid, `Err(_)` otherwise.
159	pub fn verify<Channel>(
160		&self,
161		precommit_oracle: Channel::Oracle,
162		public: &[Channel::Elem],
163		channel: &mut Channel,
164	) -> Result<(), Error>
165	where
166		F: BinaryField,
167		Channel: IOPVerifierChannel<F>,
168	{
169		let cs = &self.constraint_system;
170
171		// Check that the public input length is correct
172		if public.len() != 1 << cs.log_public() {
173			return Err(Error::IncorrectPublicInputLength {
174				expected: 1 << cs.log_public(),
175				actual: public.len(),
176			});
177		}
178
179		// Receive the private and mask oracle commitments.
180		// The private witness is witness-dependent.
181		// The mask is a fresh random draw, not witness data.
182		// It is still flagged witness-dependent so the protocol keeps treating it as secret.
183		let private_oracle = channel.recv_oracle(cs.log_private() as usize, true)?;
184		let (m_n, m_d) = cs.mask_dims();
185		let mask_oracle = channel.recv_oracle(m_n + m_d, true)?;
186
187		// Verify the multiplication constraints.
188		let MulcheckOutput {
189			a_eval,
190			b_eval,
191			c_eval,
192			mask_eval,
193			r_x,
194		} = verify_mulcheck(cs, channel)?;
195
196		// λ is the batching challenge for the constraint operands
197		let lambda = channel.sample();
198
199		// Batch together the constraint operand evaluation claims.
200		let batched_sum = evaluate_univariate(&[a_eval, b_eval, c_eval], &lambda);
201
202		// Compute rₓ^⊤ (M_A + λ M_B + λ² M_C) x. Shared via `Rc` so the transparent closures can
203		// own it and outlive this call (the opening is deferred to the channel's `finish()`). `Rc`
204		// suffices over `Arc` because the closures (`TransparentEvalFn`, no `Send` bound) and the
205		// channel never cross a thread boundary.
206		let r_x_tensor: Rc<[Channel::Elem]> = eq_ind_partial_eval_scalars(&r_x).into();
207
208		// The public segment's contribution to the operand evaluations.
209		let public_eval =
210			wiring::evaluate_wiring_mle_public(cs.mul_constraints(), public, &lambda, &r_x_tensor);
211
212		// Prover sends the precommit segment's contribution to the operand evaluations.
213		let precommit_claim = channel.recv_one()?;
214
215		let private_claim = batched_sum - public_eval - precommit_claim.clone();
216
217		// Build one transparent closure per oracle relation.
218		// The two wiring closures share the multiplication constraints through a single `Rc`.
219		// They own this handle and are `'static`, outliving this call for the deferred opening.
220		let mul_constraints: Rc<[_]> = cs.mul_constraints().into();
221		let precommit_transparent = wiring::eval_transparent(
222			mul_constraints.clone(),
223			WitnessSegment::Precommit,
224			r_x_tensor.clone(),
225			lambda.clone(),
226		);
227		let private_transparent =
228			wiring::eval_transparent(mul_constraints, WitnessSegment::Private, r_x_tensor, lambda);
229		let mask_transparent = mask_transparent(cs, &r_x);
230
231		// Verify all oracle relations
232		channel.verify_oracle_relation(precommit_oracle, precommit_transparent, precommit_claim)?;
233		channel.verify_oracle_relation(private_oracle, private_transparent, private_claim)?;
234		channel.verify_oracle_relation(mask_oracle, mask_transparent, mask_eval)?;
235
236		Ok(())
237	}
238}
239
240impl<F, H> Verifier<F, H>
241where
242	F: BinaryField,
243	H: HashSuite,
244	Output<H::LeafHash>: DeserializeBytes,
245{
246	/// Constructs a verifier for a constraint system.
247	///
248	/// See [`Verifier`] struct documentation for details.
249	pub fn setup(
250		constraint_system: ConstraintSystem<F>,
251		log_inv_rate: usize,
252	) -> Result<Self, Error> {
253		// Modify the constraint system for zero-knowledge.
254		let n_test_queries = fri::calculate_n_test_queries(SECURITY_BITS, log_inv_rate);
255		let blinding_info = BlindingInfo::for_fri_queries(n_test_queries);
256		let constraint_system = ConstraintSystemPadded::new(constraint_system, blinding_info);
257
258		let iop_verifier = IOPVerifier::new(constraint_system);
259		let oracle_specs = iop_verifier.oracle_specs();
260
261		let merkle_scheme = BinaryMerkleTreeScheme::<F, H>::new();
262
263		// Create the BaseFold ZK compiler for IOP verification
264		let basefold_compiler = BaseFoldVerifierCompiler::new(
265			&merkle_scheme,
266			oracle_specs,
267			log_inv_rate,
268			n_test_queries,
269			&MinProofSizeStrategy,
270		);
271
272		Ok(Self {
273			iop_verifier,
274			basefold_compiler,
275			_hash_marker: PhantomData,
276		})
277	}
278
279	/// Returns a reference to the IOP verifier.
280	pub const fn iop_verifier(&self) -> &IOPVerifier<F> {
281		&self.iop_verifier
282	}
283
284	pub const fn constraint_system(&self) -> &ConstraintSystemPadded<F> {
285		self.iop_verifier.constraint_system()
286	}
287
288	/// Returns a reference to the BaseFold ZK verifier compiler.
289	pub const fn iop_compiler(&self) -> &BaseFoldVerifierCompiler<F> {
290		&self.basefold_compiler
291	}
292
293	/// Verifies a proof against the constraint system.
294	///
295	/// # Arguments
296	///
297	/// * `public` - The public inputs to the constraint system
298	/// * `transcript` - The verifier transcript for Fiat-Shamir
299	///
300	/// # Preconditions
301	///
302	/// * The public input length must match the constraint system's public input size
303	pub fn verify<Challenger_: Challenger>(
304		&self,
305		public: &[F],
306		transcript: &mut VerifierTranscript<Challenger_>,
307	) -> Result<(), Error> {
308		// Verifier observes the public input (includes it in Fiat-Shamir).
309		transcript.observe().write_slice(public);
310
311		// Create channel, receive the precommit oracle, and delegate to IOPVerifier::verify. The
312		// IOP verifier only queues the oracle relations; `finish` runs the single combined opening.
313		let mut channel = self
314			.basefold_compiler
315			.create_channel_from_transcript::<H, Challenger_, _>(transcript);
316		let precommit_oracle =
317			channel.recv_oracle(self.constraint_system().log_precommit() as usize, true)?;
318		self.iop_verifier
319			.verify(precommit_oracle, public, &mut channel)?;
320		channel.finish()?;
321		Ok(())
322	}
323}
324
325fn verify_mulcheck<F, C>(
326	cs: &ConstraintSystemPadded<F>,
327	channel: &mut C,
328) -> Result<MulcheckOutput<C::Elem>, Error>
329where
330	F: BinaryField,
331	C: IPVerifierChannel<F>,
332{
333	let log_mul_constraints = checked_log_2(cs.mul_constraints().len());
334
335	// Sample random evaluation point
336	let r_mulcheck = channel.sample_many(log_mul_constraints);
337
338	// Verify the zerocheck for the multiplication constraints.
339	let mlecheck::VerifyZKOutput {
340		eval,
341		mask_eval,
342		challenges: mut r_x,
343	} = mlecheck::verify_zk(&r_mulcheck, 2, C::Elem::zero(), channel)?;
344
345	// Reverse because sumcheck binds high-to-low variable indices.
346	r_x.reverse();
347
348	// Read the claimed evaluations
349	let [a_eval, b_eval, c_eval] = channel.recv_array()?;
350
351	channel.assert_zero(a_eval.clone() * b_eval.clone() - c_eval.clone() - eval)?;
352
353	Ok(MulcheckOutput {
354		a_eval,
355		b_eval,
356		c_eval,
357		mask_eval,
358		r_x,
359	})
360}
361
362/// Returns a closure that evaluates the mask transparent polynomial at a given point.
363fn mask_transparent<F: Field, E: FieldOps + 'static>(
364	cs: &ConstraintSystemPadded<F>,
365	r_x: &[E],
366) -> binius_iop::channel::TransparentEvalFn<E> {
367	let (_m_n, m_d) = cs.mask_dims();
368	let n_vars = r_x.len();
369	let mask_degree = 2; // quadratic composition
370	let r_x = r_x.to_vec();
371
372	Box::new(move |point: &[E]| {
373		// Split into query_k (low-order bits) and query_j (high-order bits).
374		let (query_k, query_j) = point.split_at(m_d);
375
376		mlecheck::libra_eval(&r_x, query_j, query_k, n_vars, mask_degree)
377	})
378}
379
380#[derive(Debug, thiserror::Error)]
381pub enum Error {
382	#[error("FRI error: {0}")]
383	FRI(#[from] fri::Error),
384	#[error("Sumcheck error: {0}")]
385	Sumcheck(#[from] sumcheck::Error),
386	#[error("BaseFold error: {0}")]
387	BaseFold(#[from] basefold::Error),
388	#[error("Transcript error: {0}")]
389	Transcript(#[from] binius_transcript::Error),
390	#[error("IOP channel error: {0}")]
391	IOPChannel(#[from] binius_iop::channel::Error),
392	#[error("IP channel error: {0}")]
393	IPChannel(#[from] binius_ip::channel::Error),
394	#[error("incorrect public inputs length: expected {expected}, got {actual}")]
395	IncorrectPublicInputLength { expected: usize, actual: usize },
396	#[error("incorrect reduction output of the multiplication check")]
397	IncorrectMulCheckEvaluation,
398}