Skip to main content

binius_iop_prover/
merkle_channel.rs

1// Copyright 2026 The Binius Developers
2
3//! Channel abstraction for provers of protocols using Merkle commitments.
4//!
5//! This module provides the [`MerkleIPProverChannel`] trait, the prover-side counterpart of
6//! `binius_iop::merkle_channel::MerkleIPVerifierChannel`. It extends [`IPProverChannel`] with the
7//! ability to send Merkle commitments and openings of the committed leaves.
8//!
9//! The [`ProverMerkleTranscriptChannel`] implementation wraps a [`ProverTranscript`] and commits
10//! with a [`BinaryMerkleTreeProver`]: commitment roots are written as observed messages, while
11//! opening proofs are written as unobserved decommitment advice bound to the already-observed
12//! roots.
13
14use std::{borrow::BorrowMut, marker::PhantomData};
15
16use binius_compute::{Allocator, GlobalAllocator};
17use binius_core::word::Word;
18use binius_field::{Field, PackedField};
19use binius_hash_prover::{BinaryMerkleTree, ParallelHashSuite};
20use binius_iop::merkle_tree::MerkleTreeScheme;
21use binius_ip_prover::channel::{IPProverChannel, WordIPProverChannel};
22use binius_math::FieldSlice;
23use binius_transcript::{ProverTranscript, fiat_shamir::Challenger};
24use binius_utils::{SerializeBytes, checked_arithmetics::checked_log_2};
25use digest::Output;
26
27use crate::{
28	channel::grinding::GrindingProverChannel,
29	merkle_tree::{MerkleTreeProver, prover::BinaryMerkleTreeProver},
30};
31
32/// An extension of [`WordIPProverChannel`] that can send and open Merkle commitments.
33///
34/// Query indices are [`Self::Word`](WordIPProverChannel::Word)s, matching the verifier's
35/// `MerkleIPVerifierChannel`.
36pub trait MerkleIPProverChannel<F: Field>: WordIPProverChannel<F> {
37	/// A Merkle commitment, carrying the data required to open it later.
38	type Commitment;
39
40	/// Commits `data` as a Merkle tree with leaves of exactly `leaf_size` scalars each and sends
41	/// the commitment.
42	///
43	/// The tree depth is `log2(data.len() / leaf_size)`.
44	///
45	/// ## Preconditions
46	///
47	/// * `data.len()` must be a multiple of `leaf_size`, and the resulting leaf count must be a
48	///   power of two.
49	fn send_merkle_commitment<P: PackedField<Scalar = F>>(
50		&mut self,
51		data: FieldSlice<'_, P>,
52		leaf_size: usize,
53	) -> Self::Commitment;
54
55	/// Sends a multi-opening of committed leaves, bound by a Merkle commitment.
56	///
57	/// All indices must be less than `2^depth` for the commitment's tree depth. The verifier
58	/// receives `indices.len() * leaf_size` field elements via its matching `recv_openings` call.
59	///
60	/// ## Preconditions
61	///
62	/// * `data` must be the buffer passed to [`Self::send_merkle_commitment`] for this commitment.
63	fn send_openings<P: PackedField<Scalar = F>>(
64		&mut self,
65		commitment: &Self::Commitment,
66		data: FieldSlice<'_, P>,
67		indices: &[Self::Word],
68	);
69
70	/// Sends the full committed vector, bound by a Merkle commitment.
71	///
72	/// ## Preconditions
73	///
74	/// * `data` must be the buffer passed to [`Self::send_merkle_commitment`] for this commitment.
75	fn send_committed_vector<P: PackedField<Scalar = F>>(
76		&mut self,
77		commitment: &Self::Commitment,
78		data: FieldSlice<'_, P>,
79	);
80}
81
82/// A [`MerkleIPProverChannel`] over a [`ProverTranscript`], committing with a
83/// [`BinaryMerkleTreeProver`].
84///
85/// The transcript is held through a [`BorrowMut`] bound, so the channel can own the transcript or
86/// mutably borrow one.
87pub struct ProverMerkleTranscriptChannel<
88	T,
89	Challenger_,
90	F,
91	H: ParallelHashSuite,
92	A: Allocator = GlobalAllocator,
93> {
94	transcript: T,
95	merkle_prover: BinaryMerkleTreeProver<F, H, A>,
96	_challenger_marker: PhantomData<Challenger_>,
97}
98
99impl<T, Challenger_, F, H: ParallelHashSuite> ProverMerkleTranscriptChannel<T, Challenger_, F, H> {
100	/// Constructs a channel over the transcript with a default Merkle tree prover.
101	pub fn new(transcript: T) -> Self {
102		Self::with_merkle_prover(transcript, BinaryMerkleTreeProver::new())
103	}
104}
105
106impl<T, Challenger_, F, H: ParallelHashSuite, A: Allocator>
107	ProverMerkleTranscriptChannel<T, Challenger_, F, H, A>
108{
109	/// Constructs a channel over the transcript with the given Merkle tree prover.
110	///
111	/// The prover carries the allocator every committed tree draws its nodes from.
112	pub const fn with_merkle_prover(
113		transcript: T,
114		merkle_prover: BinaryMerkleTreeProver<F, H, A>,
115	) -> Self {
116		Self {
117			transcript,
118			merkle_prover,
119			_challenger_marker: PhantomData,
120		}
121	}
122
123	/// Returns the wrapped transcript.
124	pub fn into_transcript(self) -> T {
125		self.transcript
126	}
127}
128
129/// A Merkle commitment produced by [`ProverMerkleTranscriptChannel`], carrying the committed tree
130/// required to open it.
131pub struct ProverMerkleCommitment<Committed> {
132	committed: Committed,
133	depth: usize,
134	log_leaf_size: usize,
135}
136
137impl<Committed> ProverMerkleCommitment<Committed> {
138	/// Wraps a tree committed with [`MerkleTreeProver::commit_field_buffer`] (or
139	/// `commit_iterated`) into the handle the channel opens with.
140	///
141	/// `depth` is the commitment's tree depth and `log_leaf_size` the base-2 logarithm of the
142	/// leaf size the tree was committed at. A prover that has to drop a committed tree between
143	/// sending its root and opening it commits the same data again with the same
144	/// [`MerkleTreeProver`] and wraps the result here; the root it gets back is the one it sent.
145	pub const fn new(committed: Committed, depth: usize, log_leaf_size: usize) -> Self {
146		Self {
147			committed,
148			depth,
149			log_leaf_size,
150		}
151	}
152}
153
154impl<F, T, Challenger_, H, A> IPProverChannel<F>
155	for ProverMerkleTranscriptChannel<T, Challenger_, F, H, A>
156where
157	F: Field,
158	T: BorrowMut<ProverTranscript<Challenger_>>,
159	Challenger_: Challenger,
160	H: ParallelHashSuite,
161	A: Allocator,
162{
163	fn send_one(&mut self, elem: F) {
164		self.transcript.borrow_mut().send_one(elem);
165	}
166
167	fn send_many(&mut self, elems: &[F]) {
168		self.transcript.borrow_mut().send_many(elems);
169	}
170
171	fn observe_one(&mut self, val: F) {
172		self.transcript.borrow_mut().observe_one(val);
173	}
174
175	fn observe_many(&mut self, vals: &[F]) {
176		self.transcript.borrow_mut().observe_many(vals);
177	}
178
179	fn sample(&mut self) -> F {
180		IPProverChannel::sample(self.transcript.borrow_mut())
181	}
182}
183
184impl<F, T, Challenger_, H, A> WordIPProverChannel<F>
185	for ProverMerkleTranscriptChannel<T, Challenger_, F, H, A>
186where
187	F: Field,
188	T: BorrowMut<ProverTranscript<Challenger_>>,
189	Challenger_: Challenger,
190	H: ParallelHashSuite,
191	A: Allocator,
192{
193	type Word = Word;
194
195	fn observe_words(&mut self, words: &[Word]) {
196		WordIPProverChannel::<F>::observe_words(self.transcript.borrow_mut(), words);
197	}
198
199	fn sample_bits(&mut self, bits: usize) -> Word {
200		WordIPProverChannel::<F>::sample_bits(self.transcript.borrow_mut(), bits)
201	}
202}
203
204impl<T, Challenger_, F, H: ParallelHashSuite, A: Allocator> GrindingProverChannel
205	for ProverMerkleTranscriptChannel<T, Challenger_, F, H, A>
206where
207	T: BorrowMut<ProverTranscript<Challenger_>>,
208	Challenger_: Challenger + Clone,
209{
210	fn grind(&mut self, bits: usize) -> u64 {
211		// Zero difficulty is not a grind, so no nonce goes out and the challenger does not move.
212		if bits == 0 {
213			return 0;
214		}
215		self.transcript.borrow_mut().grind(bits)
216	}
217}
218
219impl<F, T, Challenger_, H, A> MerkleIPProverChannel<F>
220	for ProverMerkleTranscriptChannel<T, Challenger_, F, H, A>
221where
222	F: Field,
223	T: BorrowMut<ProverTranscript<Challenger_>>,
224	Challenger_: Challenger,
225	H: ParallelHashSuite,
226	A: Allocator,
227	Output<H::LeafHash>: SerializeBytes,
228{
229	type Commitment = ProverMerkleCommitment<BinaryMerkleTree<Output<H::LeafHash>, A>>;
230
231	fn send_merkle_commitment<P: PackedField<Scalar = F>>(
232		&mut self,
233		data: FieldSlice<'_, P>,
234		leaf_size: usize,
235	) -> Self::Commitment {
236		assert!(leaf_size.is_power_of_two(), "precondition: leaf_size must be a power of two");
237		let log_leaf_size = checked_log_2(leaf_size);
238		let (commitment, committed) = self.merkle_prover.commit_field_buffer(data, log_leaf_size);
239		self.transcript
240			.borrow_mut()
241			.message()
242			.write(&commitment.root);
243		ProverMerkleCommitment::new(committed, commitment.depth, log_leaf_size)
244	}
245
246	fn send_openings<P: PackedField<Scalar = F>>(
247		&mut self,
248		commitment: &Self::Commitment,
249		data: FieldSlice<'_, P>,
250		indices: &[Word],
251	) {
252		let tree_depth = commitment.depth;
253		debug_assert_eq!(tree_depth, data.log_len() - commitment.log_leaf_size);
254		let indices = indices
255			.iter()
256			.map(|index| index.as_u64() as usize)
257			.collect::<Vec<_>>();
258		assert!(indices.iter().all(|&index| index < 1 << tree_depth)); // precondition
259
260		// Write the optimal internal layer once, then the leaf values and opening proof for each
261		// queried index, mirroring the verifier's `recv_openings`.
262		let scheme = self.merkle_prover.scheme();
263		let layer_depth = scheme.optimal_verify_layer(indices.len(), tree_depth);
264		let layer = self.merkle_prover.layer(&commitment.committed, layer_depth);
265		let mut advice = self.transcript.borrow_mut().decommitment();
266		advice.write_slice(layer);
267		for &index in &indices {
268			let leaf = data.chunk(commitment.log_leaf_size, index);
269			advice.write_scalar_iter(leaf.iter_scalars());
270			self.merkle_prover.prove_opening(
271				&commitment.committed,
272				layer_depth,
273				index,
274				&mut advice,
275			);
276		}
277	}
278
279	fn send_committed_vector<P: PackedField<Scalar = F>>(
280		&mut self,
281		commitment: &Self::Commitment,
282		data: FieldSlice<'_, P>,
283	) {
284		debug_assert_eq!(commitment.depth, data.log_len() - commitment.log_leaf_size);
285
286		// The data itself is the whole opening.
287		// The verifier recomputes the root from it, so no further advice follows.
288		let mut advice = self.transcript.borrow_mut().decommitment();
289		advice.write_scalar_iter(data.iter_scalars());
290	}
291}
292
293#[cfg(test)]
294mod tests {
295	use binius_core::word::Word;
296	use binius_field::{Ghash128b as B128, PackedGhash2x128b};
297	use binius_hash::{StdDigest, StdHashSuite};
298	use binius_iop::{
299		channel::grinding::GrindingVerifierChannel,
300		merkle_channel::{MerkleIPVerifierChannel, VerifierMerkleTranscriptChannel},
301	};
302	use binius_ip::channel::{IPVerifierChannel, WordIPVerifierChannel};
303	use binius_math::{FieldBuffer, test_utils::random_scalars};
304	use binius_transcript::{
305		Error as TranscriptError, ProverTranscript, fiat_shamir::HasherChallenger,
306	};
307	use rand::prelude::*;
308
309	use super::{
310		GrindingProverChannel, IPProverChannel, MerkleIPProverChannel, ProverMerkleCommitment,
311		ProverMerkleTranscriptChannel,
312	};
313	use crate::merkle_tree::{MerkleTreeProver, prover::BinaryMerkleTreeProver};
314
315	type StdChallenger = HasherChallenger<StdDigest>;
316	type P = PackedGhash2x128b;
317	type VerifierChannel<T> = VerifierMerkleTranscriptChannel<T, StdChallenger, B128, StdHashSuite>;
318	type ProverChannel<T> = ProverMerkleTranscriptChannel<T, StdChallenger, B128, StdHashSuite>;
319
320	const LOG_LEN: usize = 8;
321	const LOG_LEAF_SIZE: usize = 2;
322	const LEAF_SIZE: usize = 1 << LOG_LEAF_SIZE;
323	const DEPTH: usize = LOG_LEN - LOG_LEAF_SIZE;
324	const N_QUERIES: usize = 5;
325
326	fn sample_indices<Channel: MerkleIPProverChannel<B128>>(
327		channel: &mut Channel,
328	) -> Vec<Channel::Word> {
329		(0..N_QUERIES).map(|_| channel.sample_bits(DEPTH)).collect()
330	}
331
332	#[test]
333	fn test_merkle_channel_roundtrip() {
334		let mut rng = StdRng::seed_from_u64(0);
335
336		let scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
337		let data = FieldBuffer::<P, _>::from_values(&scalars);
338
339		// Prover side: commit, sample query indices, open them, then send the vector in full.
340		let mut prover_channel =
341			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
342		let commitment = prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
343		let indices = sample_indices(&mut prover_channel);
344		prover_channel.send_openings(&commitment, data.as_view(), &indices);
345		prover_channel.send_committed_vector(&commitment, data.as_view());
346
347		// Verifier side: mirror the interaction and check the opened values against the data.
348		let transcript = prover_channel.into_transcript().into_verifier();
349		let mut verifier_channel = VerifierChannel::new(transcript);
350		let commitment = verifier_channel
351			.recv_merkle_commitment(LEAF_SIZE, DEPTH)
352			.unwrap();
353		let verifier_indices = (0..N_QUERIES)
354			.map(|_| verifier_channel.sample_bits(DEPTH))
355			.collect::<Vec<_>>();
356		assert_eq!(verifier_indices, indices);
357
358		let values = verifier_channel
359			.recv_openings(&commitment, &indices)
360			.unwrap();
361		assert_eq!(values.len(), N_QUERIES * LEAF_SIZE);
362		for (chunk, index) in values.chunks(LEAF_SIZE).zip(&indices) {
363			let index = index.as_u64() as usize;
364			assert_eq!(chunk, &scalars[index * LEAF_SIZE..(index + 1) * LEAF_SIZE]);
365		}
366
367		let vector = verifier_channel.recv_committed_vector(&commitment).unwrap();
368		assert_eq!(vector, scalars);
369
370		verifier_channel.into_transcript().finalize().unwrap();
371	}
372
373	#[test]
374	fn a_commitment_rebuilt_after_it_was_sent_opens_like_the_original() {
375		// Invariant: a commitment is a function of the data and the leaf size alone, so a handle
376		// wrapped around a tree committed again after the sent one was dropped opens to the same
377		// bytes, and the root it comes with is the root the verifier reads.
378		//
379		// Fixture state: one committed buffer, opened at the same sampled indices through the
380		// sent handle and through a rebuilt one.
381		let mut rng = StdRng::seed_from_u64(0);
382		let scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
383		let data = FieldBuffer::<P, _>::from_values(&scalars);
384		let rebuild = || {
385			let (commitment, tree) = BinaryMerkleTreeProver::<B128, StdHashSuite>::new()
386				.commit_field_buffer(data.as_view(), LOG_LEAF_SIZE);
387			(commitment.root, ProverMerkleCommitment::new(tree, commitment.depth, LOG_LEAF_SIZE))
388		};
389
390		let open_through = |rebuilt: bool| {
391			let mut prover_channel =
392				ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
393			let sent = prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
394			let commitment = if rebuilt {
395				drop(sent);
396				rebuild().1
397			} else {
398				sent
399			};
400			let indices = sample_indices(&mut prover_channel);
401			prover_channel.send_openings(&commitment, data.as_view(), &indices);
402			prover_channel.into_transcript().finalize()
403		};
404		assert_eq!(open_through(true), open_through(false));
405
406		let mut prover_channel =
407			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
408		prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
409		let transcript = prover_channel.into_transcript().into_verifier();
410		let mut verifier_channel = VerifierChannel::new(transcript);
411		let received = verifier_channel
412			.recv_merkle_commitment(LEAF_SIZE, DEPTH)
413			.unwrap();
414		assert_eq!(received.commitment.root, rebuild().0);
415	}
416
417	#[test]
418	fn a_grind_round_trips_and_zero_bits_is_not_a_grind() {
419		// Invariant: the two sides read the same difficulty out of the same parameters, so a grind
420		// has to leave the channels in step. And a difficulty of zero must be indistinguishable
421		// from never having grinding in the protocol at all, since that is what keeps
422		// `Grinding::NONE` free.
423		//
424		// Fixture state: four bits, which costs sixteen challenger trials on average.
425		const BITS: usize = 4;
426
427		let mut prover_channel =
428			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
429		prover_channel.grind(BITS);
430		let challenge = IPProverChannel::<B128>::sample(&mut prover_channel);
431
432		let mut transcript = ProverChannel::into_transcript(prover_channel).into_verifier();
433		{
434			let mut verifier_channel = VerifierChannel::new(&mut transcript);
435			verifier_channel.verify_grind(BITS).unwrap();
436			// The challenger absorbed the same nonce on both sides, so the next draw agrees.
437			assert_eq!(IPVerifierChannel::<B128>::sample(&mut verifier_channel), challenge);
438		}
439		transcript.finalize().unwrap();
440
441		// A zero-bit grind writes no nonce and moves no challenger, so the two transcripts below
442		// are byte for byte the same and both sample the same challenge.
443		let write = |bits: Option<usize>| {
444			let mut channel = ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
445			if let Some(bits) = bits {
446				channel.grind(bits);
447			}
448			let challenge = IPProverChannel::<B128>::sample(&mut channel);
449			(ProverChannel::into_transcript(channel).finalize(), challenge)
450		};
451		assert_eq!(write(Some(0)), write(None));
452	}
453
454	#[test]
455	fn a_grind_checked_at_the_wrong_difficulty_is_rejected() {
456		// Invariant: the sampler reads four bytes whatever the difficulty, so a mismatch leaves
457		// both challengers in step and shows up only as work the prover never did.
458		//
459		// Fixture state: four bits are ground and twenty-four demanded, a gap wide enough that a
460		// nonce satisfying the larger demand by accident turns up once in a million.
461		let mut prover_channel =
462			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
463		prover_channel.grind(4);
464
465		let mut transcript = ProverChannel::into_transcript(prover_channel).into_verifier();
466		let mut verifier_channel = VerifierChannel::new(&mut transcript);
467		let err = verifier_channel
468			.verify_grind(24)
469			.expect_err("four bits of work cannot satisfy a twenty-four bit demand");
470		let TranscriptError::InsufficientWork { bits, sampled } = err else {
471			panic!("expected unmet proof of work, got {err}")
472		};
473		assert_eq!(bits, 24);
474		assert_ne!(sampled, 0);
475	}
476
477	#[test]
478	fn test_merkle_channel_borrowed_transcript() {
479		let mut rng = StdRng::seed_from_u64(0);
480
481		let scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
482		let data = FieldBuffer::<P, _>::from_values(&scalars);
483
484		let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
485		{
486			let mut prover_channel = ProverChannel::new(&mut prover_transcript);
487			let commitment = prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
488			let indices = sample_indices(&mut prover_channel);
489			prover_channel.send_openings(&commitment, data.as_view(), &indices);
490		}
491
492		let mut verifier_transcript = prover_transcript.into_verifier();
493		{
494			let mut verifier_channel = VerifierChannel::new(&mut verifier_transcript);
495			let commitment = verifier_channel
496				.recv_merkle_commitment(LEAF_SIZE, DEPTH)
497				.unwrap();
498			let indices = (0..N_QUERIES)
499				.map(|_| verifier_channel.sample_bits(DEPTH))
500				.collect::<Vec<_>>();
501			let values = verifier_channel
502				.recv_openings(&commitment, &indices)
503				.unwrap();
504			for (chunk, index) in values.chunks(LEAF_SIZE).zip(&indices) {
505				let index = index.as_u64() as usize;
506				assert_eq!(chunk, &scalars[index * LEAF_SIZE..(index + 1) * LEAF_SIZE]);
507			}
508		}
509		verifier_transcript.finalize().unwrap();
510	}
511
512	#[test]
513	fn test_merkle_channel_rejects_openings_at_wrong_index() {
514		let mut rng = StdRng::seed_from_u64(0);
515
516		let scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
517		let data = FieldBuffer::<P, _>::from_values(&scalars);
518
519		let mut prover_channel =
520			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
521		let commitment = prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
522		let indices = sample_indices(&mut prover_channel);
523		prover_channel.send_openings(&commitment, data.as_view(), &indices);
524
525		let transcript = prover_channel.into_transcript().into_verifier();
526		let mut verifier_channel = VerifierChannel::new(transcript);
527		let commitment = verifier_channel
528			.recv_merkle_commitment(LEAF_SIZE, DEPTH)
529			.unwrap();
530		let _ = (0..N_QUERIES)
531			.map(|_| verifier_channel.sample_bits(DEPTH))
532			.collect::<Vec<_>>();
533
534		// Requesting openings at indices other than the ones the prover opened must fail.
535		let wrong_indices = indices
536			.iter()
537			.map(|&index| index ^ Word::ONE)
538			.collect::<Vec<_>>();
539		assert!(
540			verifier_channel
541				.recv_openings(&commitment, &wrong_indices)
542				.is_err()
543		);
544
545		// Drop the transcript without finalizing; the tampered read left it misaligned.
546		let _ = verifier_channel.into_transcript();
547	}
548
549	#[test]
550	fn test_merkle_channel_rejects_wrong_root() {
551		let mut rng = StdRng::seed_from_u64(0);
552
553		let scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
554		let data = FieldBuffer::<P, _>::from_values(&scalars);
555		let other_scalars = random_scalars::<B128>(&mut rng, 1 << LOG_LEN);
556		let other_data = FieldBuffer::<P, _>::from_values(&other_scalars);
557
558		// Commit one buffer but open the other, so the openings do not match the commitment.
559		let mut prover_channel =
560			ProverChannel::new(ProverTranscript::new(StdChallenger::default()));
561		let commitment = prover_channel.send_merkle_commitment(data.as_view(), LEAF_SIZE);
562		let other_commitment =
563			prover_channel.send_merkle_commitment(other_data.as_view(), LEAF_SIZE);
564		let indices = sample_indices(&mut prover_channel);
565		prover_channel.send_openings(&other_commitment, other_data.as_view(), &indices);
566		let _ = commitment;
567
568		let transcript = prover_channel.into_transcript().into_verifier();
569		let mut verifier_channel = VerifierChannel::new(transcript);
570		let commitment = verifier_channel
571			.recv_merkle_commitment(LEAF_SIZE, DEPTH)
572			.unwrap();
573		let _other_commitment = verifier_channel
574			.recv_merkle_commitment(LEAF_SIZE, DEPTH)
575			.unwrap();
576		let indices = (0..N_QUERIES)
577			.map(|_| verifier_channel.sample_bits(DEPTH))
578			.collect::<Vec<_>>();
579
580		// The openings on the tape are bound to `other_commitment`, so verifying them against
581		// `commitment` must fail.
582		assert!(
583			verifier_channel
584				.recv_openings(&commitment, &indices)
585				.is_err()
586		);
587
588		let _ = verifier_channel.into_transcript();
589	}
590}