Skip to main content

binius_spartan_prover/wrapper/
zk_wrapped_prover_channel.rs

1// Copyright 2026 The Binius Developers
2
3//! ZK-wrapped prover channel that runs an inner proof and then proves the outer
4//! wrapper constraint system.
5//!
6//! [`ZKWrappedProverChannel`] wraps a [`BaseFoldProverChannel`] and records all channel values.
7//! On `send_*`/`sample`/`observe_*`, it delegates to the inner BaseFold channel and records
8//! each value. After the inner proof is run, [`finish`] replays the recorded interaction through
9//! a caller-provided closure to fill the outer witness, then runs the outer IOP prover.
10//!
11//! [`BaseFoldProverChannel`]: binius_iop_prover::basefold::channel::BaseFoldProverChannel
12//! [`finish`]: ZKWrappedProverChannel::finish
13
14use std::{iter::repeat_with, sync::Arc};
15
16use binius_compute::Allocator;
17use binius_field::{BinaryField, PackedField};
18use binius_iop::channel::OracleSpec;
19use binius_iop_prover::{
20	basefold::channel::{BaseFoldOracle, BaseFoldProverChannel},
21	channel::IOPProverChannel,
22	merkle_channel::MerkleIPProverChannel,
23};
24use binius_ip_prover::channel::{IPProverChannel, WordIPProverChannel};
25use binius_math::{FieldSlice, FieldVec, StructuredBuffer, ntt::AdditiveNTT};
26use binius_spartan_frontend::constraint_system::WitnessLayout;
27use binius_spartan_verifier::IOPVerifier;
28use rand::CryptoRng;
29
30use crate::{Error, IOPProver, pack_and_blind_witness, wrapper::ReplayChannel};
31
32/// A prover channel that wraps a [`BaseFoldProverChannel`] and an outer Spartan IOP prover.
33///
34/// This channel records all channel values. On
35/// `send_*`/`sample`/`observe_*`, it delegates to the inner BaseFold channel and records each
36/// value. After the inner proof is run through this channel, call
37/// [`finish`](Self::finish) to replay the interaction, fill the outer witness, and generate the
38/// outer proof.
39///
40/// The `ReplayFn` closure is called during [`finish`](Self::finish) with a [`ReplayChannel`] to
41/// replay the inner verification and fill the outer witness. This allows the channel to be generic
42/// over different inner verification protocols.
43pub struct ZKWrappedProverChannel<'a, P, NTT, Channel, ReplayFn, A>
44where
45	P: PackedField<Scalar: BinaryField>,
46	NTT: AdditiveNTT<Field = P::Scalar> + Sync,
47	Channel: MerkleIPProverChannel<P::Scalar>,
48	A: Allocator,
49{
50	inner_channel: BaseFoldProverChannel<'a, P::Scalar, P, NTT, Channel, A>,
51	outer_prover: &'a IOPProver<P::Scalar>,
52	/// Allocator for the outer proof's working buffers, borrowed from the owning prover so it
53	/// outlives this per-proof channel. Used in [`Self::finish`].
54	alloc: &'a A,
55	outer_layout: Arc<WitnessLayout<P::Scalar>>,
56	replay_fn: ReplayFn,
57	keys: Vec<P::Scalar>,
58	next_key_idx: usize,
59	interaction: Vec<P::Scalar>,
60	/// Handle to the outer precommit oracle committed at construction time. The buffer
61	/// (`precommit_packed`) is purely random — it is the one-time-pad encryption key for the
62	/// outer encrypted transcript (to be wired up in a follow-up; for now the outer circuit has
63	/// no precommit wires that reference it).
64	precommit_oracle: BaseFoldOracle,
65	precommit_packed: FieldVec<P, A>,
66	/// Number of outer oracles still to be committed on `inner_channel` during `finish` (the
67	/// outer prover's non-precommit oracles — private and mask).
68	n_outer_suffix_oracles: usize,
69}
70
71impl<'a, F, P, NTT, Channel, ReplayFn, A> ZKWrappedProverChannel<'a, P, NTT, Channel, ReplayFn, A>
72where
73	F: BinaryField,
74	P: PackedField<Scalar = F>,
75	NTT: AdditiveNTT<Field = F> + Sync,
76	Channel: MerkleIPProverChannel<F>,
77	A: Allocator,
78{
79	/// Creates a new ZK-wrapped prover channel.
80	///
81	/// Commits the outer prover's precommit oracle on the inner channel as part of construction:
82	/// a random [`FieldBuffer<P>`](binius_math::FieldBuffer) the size of the outer precommit oracle
83	/// segment is sent to the channel and kept for use in [`Self::finish`]. This random buffer is
84	/// the one-time-pad encryption key for the (future) outer encrypted transcript.
85	///
86	/// The inner channel's oracle specs are expected to be laid out as
87	/// `[outer_precommit, inner..., outer_private, outer_mask]`.
88	///
89	/// # Arguments
90	///
91	/// * `inner_channel` - The BaseFold ZK channel with oracle specs for both inner and outer
92	///   proofs
93	/// * `outer_prover` - The IOP prover for the outer (wrapper) constraint system
94	/// * `outer_layout` - The witness layout for the outer constraint system
95	/// * `alloc` - Allocator for the outer proof's working buffers, borrowed from the owning prover
96	/// * `rng` - RNG used to generate the random precommit buffer (the future OTP key)
97	/// * `replay_fn` - Closure called during [`finish`](Self::finish) with a [`ReplayChannel`] to
98	///   replay the inner verification and fill the outer witness
99	pub fn new(
100		mut inner_channel: BaseFoldProverChannel<'a, F, P, NTT, Channel, A>,
101		outer_prover: &'a IOPProver<F>,
102		outer_layout: Arc<WitnessLayout<F>>,
103		alloc: &'a A,
104		rng: impl CryptoRng,
105		replay_fn: ReplayFn,
106	) -> Self {
107		let outer_oracle_specs =
108			IOPVerifier::new(outer_prover.constraint_system().clone()).oracle_specs();
109		let all_specs = inner_channel.remaining_oracle_specs();
110		let n_outer = outer_oracle_specs.len();
111		assert!(
112			n_outer >= 1 && all_specs.len() >= n_outer,
113			"outer oracle specs ({n_outer}) exceed channel oracle specs ({}) or are empty",
114			all_specs.len(),
115		);
116		assert_eq!(
117			all_specs[0], outer_oracle_specs[0],
118			"outer precommit oracle spec must be the first spec on the channel",
119		);
120		let suffix_len = n_outer - 1;
121		assert_eq!(
122			&all_specs[all_specs.len() - suffix_len..],
123			&outer_oracle_specs[1..],
124			"outer private/mask oracle specs must be the final suffix of channel specs",
125		);
126
127		let (keys, precommit_oracle, precommit_packed) = {
128			let _scope = tracing::debug_span!("Commit Transcript Mask").entered();
129			Self::commit_transcript_mask(&mut inner_channel, outer_prover, alloc, rng)
130		};
131
132		Self {
133			inner_channel,
134			outer_prover,
135			alloc,
136			outer_layout,
137			replay_fn,
138			keys,
139			next_key_idx: 0,
140			interaction: Vec::new(),
141			precommit_oracle,
142			precommit_packed,
143			n_outer_suffix_oracles: suffix_len,
144		}
145	}
146
147	/// Commits random OTP keys as the outer precommit oracle. Each key encrypts one element sent by
148	/// the inner prover through this wrapped channel; the outer CS (built symbolically from the
149	/// inner verifier) contains a matching precommit wire per key that the outer proof uses to
150	/// decrypt.
151	fn commit_transcript_mask(
152		inner_channel: &mut BaseFoldProverChannel<'a, F, P, NTT, Channel, A>,
153		outer_prover: &IOPProver<F>,
154		alloc: &A,
155		mut rng: impl CryptoRng,
156	) -> (Vec<F>, BaseFoldOracle, FieldVec<P, A>) {
157		let cs = outer_prover.constraint_system();
158		let keys = repeat_with(|| F::random(&mut rng))
159			.take(cs.n_precommit() as usize)
160			.collect::<Vec<F>>();
161		let precommit_blinding = *cs.blinding_info();
162		let precommit_packed = pack_and_blind_witness::<_, _, P>(
163			alloc,
164			cs.log_precommit() as usize,
165			&keys,
166			cs.n_precommit() as usize,
167			&precommit_blinding,
168			&mut rng,
169		);
170		let precommit_oracle = inner_channel.send_oracle(precommit_packed.as_view());
171		(keys, precommit_oracle, precommit_packed)
172	}
173
174	fn next_key(&mut self) -> F {
175		let key = self.keys[self.next_key_idx];
176		self.next_key_idx += 1;
177		key
178	}
179
180	/// Consumes the channel and runs the outer proof.
181	///
182	/// This should be called after the inner proof has been run through this channel.
183	/// It:
184	/// 1. Creates a [`ReplayChannel`] from the recorded interaction
185	/// 2. Calls the `replay_fn` closure to replay the inner verification and fill the outer witness
186	/// 3. Validates and generates the outer IOP proof
187	pub fn finish(self, rng: impl CryptoRng) -> Result<(), Error>
188	where
189		ReplayFn: FnOnce(&mut ReplayChannel<F>),
190	{
191		let Self {
192			mut inner_channel,
193			outer_prover,
194			alloc,
195			outer_layout,
196			replay_fn,
197			keys,
198			interaction,
199			precommit_oracle,
200			precommit_packed,
201			..
202		} = self;
203
204		// Replay the inner verification through the outer witness generator.
205		let witness = {
206			let _scope = tracing::debug_span!("Generating ZK wrapper witness").entered();
207			let mut replay_channel = ReplayChannel::new(outer_layout, keys, interaction);
208			replay_fn(&mut replay_channel);
209			replay_channel
210				.finish()
211				.expect("outer witness generation should not fail")
212		};
213
214		// Validate and generate the outer proof.
215		outer_prover.prove::<P, _, _>(
216			&witness,
217			precommit_oracle,
218			precommit_packed,
219			rng,
220			&mut inner_channel,
221			alloc,
222		)?;
223		// Both the inner and outer proofs queued their oracle relations onto `inner_channel`; run
224		// the single combined opening over all committed oracles now.
225		inner_channel.finish();
226		Ok(())
227	}
228}
229
230impl<F, P, NTT, Channel, ReplayFn, A> IPProverChannel<F>
231	for ZKWrappedProverChannel<'_, P, NTT, Channel, ReplayFn, A>
232where
233	F: BinaryField,
234	P: PackedField<Scalar = F>,
235	NTT: AdditiveNTT<Field = F> + Sync,
236	Channel: MerkleIPProverChannel<F>,
237	A: Allocator,
238{
239	fn send_one(&mut self, elem: F) {
240		let key = self.next_key();
241		// Encrypt the element with the OTP key before sending. Record the encrypted value in
242		// `interaction` — that's what the outer witness's inout wires hold (and what the replay
243		// side adds the key back to in order to recover the plaintext for the inner verifier).
244		let encrypted = elem + key;
245		self.inner_channel.send_one(encrypted);
246		self.interaction.push(encrypted);
247	}
248
249	fn send_public_claim(&mut self, elem: F) {
250		// A claim is a function of public values, so it is sent in the clear and consumes no OTP
251		// key. `interaction` records the plaintext, which is what the outer witness's inout wire
252		// holds and what the replay hands the inner verifier.
253		self.inner_channel.send_one(elem);
254		self.interaction.push(elem);
255	}
256
257	fn observe_one(&mut self, val: F) {
258		self.inner_channel.observe_one(val);
259		self.interaction.push(val);
260	}
261
262	fn sample(&mut self) -> F {
263		let val = self.inner_channel.sample();
264		self.interaction.push(val);
265		val
266	}
267}
268
269impl<F, P, NTT, Channel, ReplayFn, A> WordIPProverChannel<F>
270	for ZKWrappedProverChannel<'_, P, NTT, Channel, ReplayFn, A>
271where
272	F: BinaryField,
273	P: PackedField<Scalar = F>,
274	NTT: AdditiveNTT<Field = F> + Sync,
275	Channel: MerkleIPProverChannel<F>,
276	A: Allocator,
277{
278	type Word = Channel::Word;
279
280	fn observe_words(&mut self, words: &[Self::Word]) {
281		// Only the inner Fiat-Shamir state takes the words. Nothing is recorded for replay, since
282		// the replay channel observes nothing either.
283		self.inner_channel.observe_words(words);
284	}
285
286	fn sample_bits(&mut self, bits: usize) -> Self::Word {
287		self.inner_channel.sample_bits(bits)
288	}
289}
290
291impl<F, P, NTT, Channel, ReplayFn, A> IOPProverChannel<P, A>
292	for ZKWrappedProverChannel<'_, P, NTT, Channel, ReplayFn, A>
293where
294	F: BinaryField,
295	P: PackedField<Scalar = F>,
296	NTT: AdditiveNTT<Field = F> + Sync,
297	Channel: MerkleIPProverChannel<F>,
298	A: Allocator,
299{
300	type Oracle = BaseFoldOracle;
301
302	fn remaining_oracle_specs(&self) -> &[OracleSpec] {
303		let remaining = self.inner_channel.remaining_oracle_specs();
304		let n_inner_remaining = remaining.len() - self.n_outer_suffix_oracles;
305		&remaining[..n_inner_remaining]
306	}
307
308	fn send_oracle(&mut self, buffer: FieldSlice<'_, P>) -> Self::Oracle {
309		assert!(
310			!self.remaining_oracle_specs().is_empty(),
311			"send_oracle called but no inner oracle specs remaining"
312		);
313		self.inner_channel.send_oracle(buffer)
314	}
315
316	fn prove_oracle_relation(
317		&mut self,
318		oracle: Self::Oracle,
319		transparent: StructuredBuffer<P, A::Vec<P>>,
320		claim: P::Scalar,
321	) {
322		// For each oracle opening, the prover sends the decrypted evaluation. The outer verifier
323		// checks in the circuit equality of this value with the expected expression over encrypted
324		// values.
325		self.inner_channel.send_one(claim);
326		self.interaction.push(claim);
327
328		self.inner_channel
329			.prove_oracle_relation(oracle, transparent, claim);
330	}
331
332	fn finalize_oracle(&mut self, oracle: Self::Oracle, buffer: FieldVec<P, A>) {
333		self.inner_channel.finalize_oracle(oracle, buffer);
334	}
335}