referrerpolicy=no-referrer-when-downgrade

cumulus_primitives_spec_messaging/
message.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: Apache-2.0
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// 	http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17//! Off-chain wire types and their verifiers.
18//!
19//! The relay chain decodes none of this; it only matches the `StreamsRoot` commitments in the UMP
20//! signals. But the two ends of each protocol are different chains' collators, so the encodings and
21//! the envelope variant indices are part of the wire format.
22//!
23//! - [`leaf_hash`] turns a payload into an MMR leaf. A message is a bounded payload keyed by
24//! `StreamId`; source, stream and position are structural, so there is no message struct.
25//! - Fetch: [`MessagesRequest`] / [`MessagesResponse`], verified by [`verify_messages`] against a
26//! `StreamsRoot` the requester has already verified. The `extension` + `tree_proof` check is the
27//! same one the requires-lift performs.
28//! - Event read: [`EventRequest`] / [`EventResponse`], the single-leaf read of an `Ack` /
29//! `Broadcast` / `Private` stream, verified by [`verify_event`].
30//! - Envelope: [`ExchangeRequest`] / [`ExchangeResponse`] multiplex both over `/spec-msg/exchange`,
31//! verified by [`verify_exchange`], which also binds the response variant to the request's. Variant
32//! indices are frozen.
33
34use alloc::vec::Vec;
35use codec::{Decode, DecodeWithMemTracking, Encode};
36use polkadot_core_primitives::Hash;
37use scale_info::TypeInfo;
38use sp_core::ConstU32;
39use sp_runtime::{traits::Hash as HashT, BoundedVec};
40
41use crate::{
42	lift::{MMRExtensionProof, MmrInclusionProof, ProofError},
43	mmr::{MessagePosition, MmrFrontier, MmrRoot, MAX_MMR_HASHES, MAX_MMR_LEAF_COUNT},
44	stream::StreamId,
45	streams_root::{streams_root_from_proof, StreamProof, StreamsRoot},
46	SpecHasher, LEAF_TAG,
47};
48
49/// Why verifying an off-chain response failed. [`RootMismatch`](VerifyError::RootMismatch) is
50/// benign (retry under a fresher root); the rest indicate a malformed or forged response.
51#[derive(Clone, Copy, PartialEq, Eq, Debug)]
52pub enum VerifyError {
53	/// An embedded MMR proof (inclusion or extension) did not verify.
54	Proof(ProofError),
55	/// The `StreamsRoot` keyed-tree walk (`tree_proof`) did not resolve.
56	TreeProof,
57	/// Everything verified, but the recomputed `StreamsRoot` is not the requested `under`. The
58	/// peer served under a different root, stale or wrong. Not necessarily malicious.
59	RootMismatch,
60	/// The response's `base` is not the requested `start`. Only surfaced by [`verify_messages`].
61	UnexpectedBase,
62	/// The payloads total more than the request's `max_bytes`, or `max_bytes == 0` and the
63	/// response carries any payload, even an empty one. Only surfaced by [`verify_messages`].
64	ExceedsBudget,
65	/// The response envelope variant does not match the request's. Only surfaced by
66	/// [`verify_exchange`].
67	VariantMismatch,
68	/// A payload exceeded the protocol's single-message bound ([`MAX_SPECULATIVE_MESSAGE_LEN`]).
69	PayloadTooLarge,
70	/// The response is structurally malformed independently of its proofs (e.g. a `start_peaks`
71	/// set inconsistent with `base`, or a `base + len` leaf-counter overflow).
72	MalformedResponse,
73}
74
75impl From<ProofError> for VerifyError {
76	fn from(e: ProofError) -> Self {
77		VerifyError::Proof(e)
78	}
79}
80
81/// Maximum size in bytes of a single speculative message payload.
82pub const MAX_SPECULATIVE_MESSAGE_LEN: u32 = 102_400;
83
84/// Maximum payloads in one [`MessagesResponse`], enforced at decode. The byte budget does not bound
85/// the count: an empty payload costs one wire byte and no budget, yet is hashed into a leaf, so
86/// without this bound one response could carry millions. A consumer fetches the rest under a later
87/// request.
88pub const MAX_MESSAGES_PER_RESPONSE: u32 = 1024;
89
90/// Hash a payload into a stream MMR leaf: `H(LEAF_TAG ++ leaf_version ++ payload)`. Source, stream
91/// and position are structural and not in the preimage. Versions are hash-disjoint, so only the
92/// correct one reproduces a committed root.
93pub fn leaf_hash(leaf_version: u8, payload: &[u8]) -> Hash {
94	let mut preimage = Vec::with_capacity(2 + payload.len());
95	preimage.push(LEAF_TAG);
96	preimage.push(leaf_version);
97	preimage.extend_from_slice(payload);
98	<SpecHasher as HashT>::hash(&preimage)
99}
100
101/// Request for a range of a stream's messages, verified under a chosen `StreamsRoot`.
102#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
103pub struct MessagesRequest {
104	pub stream: StreamId,
105	/// Where to start; typically the receiver's frontier leaf count.
106	pub start: MessagePosition,
107	/// The `StreamsRoot` the response must verify under: one the requester has already
108	/// authenticated. Freshness is the requester's job; re-request under a newer root once
109	/// verified.
110	pub under: StreamsRoot,
111	/// Response size bound; the server may cap harder. `0` requests a payload-free proof (lift
112	/// material).
113	pub max_bytes: u32,
114}
115
116/// Response: payloads from `base` on, plus the proofs binding them and everything before them to
117/// the requested `StreamsRoot`. Nothing is trusted; fabricated peaks, payloads or proofs cannot
118/// reproduce a committed root.
119#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
120pub struct MessagesResponse {
121	/// Position of the first payload. A hint; a lie fails the proofs.
122	pub base: MessagePosition,
123	/// Leaf-format version for these payloads. A hint; only the correct version reproduces a
124	/// committed root. A response never spans a version change.
125	pub leaf_version: u8,
126	/// The payloads, in MMR order; payload `i` has position `base + i`. At most
127	/// [`MAX_MESSAGES_PER_RESPONSE`], rejected at the length prefix.
128	pub payloads: BoundedVec<Vec<u8>, ConstU32<MAX_MESSAGES_PER_RESPONSE>>,
129	/// The stream's peak set at `base`, for a consumer that holds no frontier. Fabricated peaks
130	/// cannot extend to a committed root.
131	pub start_peaks: BoundedVec<Hash, ConstU32<MAX_MMR_HASHES>>,
132	/// From the frontier recomputed over `payloads` to the stream's current root; the identity
133	/// extension when the payloads already reach it.
134	pub extension: MMRExtensionProof,
135	/// From the stream's root to the `StreamsRoot` the response verifies under.
136	pub tree_proof: StreamProof,
137}
138
139/// Verify a [`MessagesResponse`] against its [`MessagesRequest`]: `resp.base == req.start`, total
140/// payload bytes within `req.max_bytes` (`0` means proofs only), then the payloads under
141/// `req.stream` / `req.under`. On `Ok`, `resp.payloads` are verified. The public entry for the
142/// fetch protocol.
143pub fn verify_messages(req: &MessagesRequest, resp: &MessagesResponse) -> Result<(), VerifyError> {
144	if resp.base != req.start {
145		return Err(VerifyError::UnexpectedBase);
146	}
147	let total = resp.payloads.iter().map(|p| p.len() as u64).fold(0u64, u64::saturating_add);
148	if total > req.max_bytes as u64 || (req.max_bytes == 0 && !resp.payloads.is_empty()) {
149		return Err(VerifyError::ExceedsBudget);
150	}
151	verify_messages_response(req.stream, req.under, resp)
152}
153
154/// Verify a [`MessagesResponse`] for `stream` under `under`: the same check the requires-lift
155/// performs. Crate-internal: it does not bind `resp.base` to a requested start; [`verify_messages`]
156/// does.
157pub(crate) fn verify_messages_response(
158	stream: StreamId,
159	under: StreamsRoot,
160	resp: &MessagesResponse,
161) -> Result<(), VerifyError> {
162	// `base` and `start_peaks` are untrusted; `from_parts` rejects an inconsistent shape and a
163	// `base` past the ceiling.
164	let mut frontier = MmrFrontier::from_parts(resp.start_peaks.to_vec(), resp.base.0)
165		.ok_or(VerifyError::MalformedResponse)?;
166	// The frontier after the payloads must stay within `MAX_MMR_LEAF_COUNT`, or it would not
167	// decode. Checked up front; `try_append` refuses it too.
168	resp.base
169		.0
170		.checked_add(resp.payloads.len() as u64)
171		.filter(|end| *end <= MAX_MMR_LEAF_COUNT)
172		.ok_or(VerifyError::MalformedResponse)?;
173	// Defense-in-depth against a crafted response inflating the hashed input; the transport also
174	// caps total response size.
175	resp.payloads.iter().try_for_each(|p| check_payload_len(p))?;
176
177	for payload in &resp.payloads {
178		frontier
179			.try_append(leaf_hash(resp.leaf_version, payload))
180			.map_err(|_| VerifyError::MalformedResponse)?;
181	}
182
183	let current = resp.extension.calculate_root(&frontier)?;
184	bind_under(stream, under, current, &resp.tree_proof)
185}
186
187fn check_payload_len(payload: &[u8]) -> Result<(), VerifyError> {
188	if payload.len() as u64 > MAX_SPECULATIVE_MESSAGE_LEN as u64 {
189		return Err(VerifyError::PayloadTooLarge);
190	}
191	Ok(())
192}
193
194/// Walk `tree_proof` from `stream_root` to a `StreamsRoot` and require it to be `under`:
195/// [`TreeProof`](VerifyError::TreeProof) if the walk is malformed,
196/// [`RootMismatch`](VerifyError::RootMismatch) if it ends elsewhere.
197fn bind_under(
198	stream: StreamId,
199	under: StreamsRoot,
200	stream_root: MmrRoot,
201	tree_proof: &StreamProof,
202) -> Result<(), VerifyError> {
203	let root =
204		streams_root_from_proof(stream, stream_root.0, tree_proof).ok_or(VerifyError::TreeProof)?;
205	(root == under).then_some(()).ok_or(VerifyError::RootMismatch)
206}
207
208fn event_leaf(resp: &EventResponse) -> Result<Hash, VerifyError> {
209	check_payload_len(&resp.payload)?;
210	Ok(leaf_hash(resp.leaf_version, &resp.payload))
211}
212
213/// Request for a single event of a lossy stream (`Ack` / `Broadcast` / `Private`), verified under a
214/// chosen `StreamsRoot`. The server either serves or fails; nothing is resolved server-side.
215#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
216pub struct EventRequest {
217	pub stream: StreamId,
218	/// The `StreamsRoot` the response must verify under. Always explicit.
219	pub under: StreamsRoot,
220	/// A specific position, or `None` for the head as of `under`.
221	pub at: Option<MessagePosition>,
222}
223
224/// Response carrying one event leaf and the proofs placing it under the requested `StreamsRoot`.
225/// Nothing is trusted. `under` fixes the stream's leaf count, so the head is the leaf at `count -
226/// 1`; an old leaf cannot be served as the head.
227#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
228pub struct EventResponse {
229	/// The event payload; its position is proven by `inclusion`.
230	pub payload: Vec<u8>,
231	/// Leaf-format version for `payload`. A hint; only the correct version reproduces a committed
232	/// root.
233	pub leaf_version: u8,
234	/// Single-leaf inclusion proof: the sibling path plus the other peaks.
235	pub inclusion: MmrInclusionProof,
236	/// From the stream's root (derived from `inclusion`) to the `StreamsRoot` the response
237	/// verifies under.
238	pub tree_proof: StreamProof,
239}
240
241/// The `/spec-msg/exchange` request envelope. Variant indices are frozen; foreign implementations
242/// decode by them.
243#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
244pub enum ExchangeRequest {
245	/// Ordered fetching or lift material. Answered by [`ExchangeResponse::Messages`].
246	#[codec(index = 0)]
247	Messages(MessagesRequest),
248	/// Single-event read. Answered by [`ExchangeResponse::Event`].
249	#[codec(index = 1)]
250	Event(EventRequest),
251}
252
253/// The `/spec-msg/exchange` response envelope. No error variant: a server that cannot serve refuses
254/// at the transport level.
255#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
256pub enum ExchangeResponse {
257	/// Response to [`ExchangeRequest::Messages`].
258	#[codec(index = 0)]
259	Messages(MessagesResponse),
260	/// Response to [`ExchangeRequest::Event`].
261	#[codec(index = 1)]
262	Event(EventResponse),
263}
264
265/// Outcome of verifying an [`EventResponse`]. A head read yields the stream [`MmrFrontier`], which
266/// lossy consumption records as its interval endpoint; a positional read yields none, since an
267/// inclusion proof reconstructs a root, not a frontier.
268#[derive(Clone, PartialEq, Eq, Debug)]
269pub enum VerifiedEvent {
270	/// Head (register / latest-event) read: the head position and the stream frontier at that
271	/// head.
272	Head { position: MessagePosition, frontier: MmrFrontier },
273	/// Positional read: the requested position.
274	Positional { position: MessagePosition },
275}
276
277/// Verify an [`EventResponse`] against its [`EventRequest`]: `req.at` selects head (`None`) or leaf
278/// `k`, so a head response is never accepted for a positional request or vice versa. On `Ok`,
279/// `resp.payload` is verified. The public entry for event reads.
280pub fn verify_event(
281	req: &EventRequest,
282	resp: &EventResponse,
283) -> Result<VerifiedEvent, VerifyError> {
284	match req.at {
285		None => {
286			let (position, frontier) = verify_event_response(req.stream, req.under, resp)?;
287			Ok(VerifiedEvent::Head { position, frontier })
288		},
289		Some(position) => {
290			verify_positional_event_response(req.stream, req.under, position, resp)?;
291			Ok(VerifiedEvent::Positional { position })
292		},
293	}
294}
295
296/// Verify an [`EventResponse`] as the head of `stream` under `under`; returns the position and the
297/// frontier at that head. Crate-internal; [`verify_event`] is the public entry.
298///
299/// The frontier is returned because lossy consumption needs it as the interval endpoint.
300pub(crate) fn verify_event_response(
301	stream: StreamId,
302	under: StreamsRoot,
303	resp: &EventResponse,
304) -> Result<(MessagePosition, MmrFrontier), VerifyError> {
305	let (position, frontier) = resp.inclusion.verify_head(event_leaf(resp)?)?;
306	bind_under(stream, under, frontier.root(), &resp.tree_proof)?;
307	Ok((position, frontier))
308}
309
310/// Verify an [`EventResponse`] as the leaf at `position` of `stream` under `under`.
311/// Crate-internal; [`verify_event`] is the public entry. Same chain as the head read, with
312/// [`MmrInclusionProof::verify_leaf`].
313pub(crate) fn verify_positional_event_response(
314	stream: StreamId,
315	under: StreamsRoot,
316	position: MessagePosition,
317	resp: &EventResponse,
318) -> Result<(), VerifyError> {
319	let root = resp.inclusion.verify_leaf(position, event_leaf(resp)?)?;
320	bind_under(stream, under, root, &resp.tree_proof)
321}
322
323/// Verified outcome of an [`ExchangeResponse`]: a fetch, or a [`VerifiedEvent`].
324#[derive(Clone, PartialEq, Eq, Debug)]
325pub enum ExchangeVerified {
326	/// Answer to an [`ExchangeRequest::Messages`]; the response's payloads are verified.
327	Messages,
328	/// Answer to an [`ExchangeRequest::Event`]: the verified event.
329	Event(VerifiedEvent),
330}
331
332/// Verify a `/spec-msg/exchange` response against its request. The variants must match
333/// ([`VariantMismatch`](VerifyError::VariantMismatch) otherwise); then dispatches to
334/// [`verify_messages`] or [`verify_event`]. The top-level public entry.
335pub fn verify_exchange(
336	req: &ExchangeRequest,
337	resp: &ExchangeResponse,
338) -> Result<ExchangeVerified, VerifyError> {
339	match (req, resp) {
340		(ExchangeRequest::Messages(mreq), ExchangeResponse::Messages(mresp)) => {
341			verify_messages(mreq, mresp).map(|()| ExchangeVerified::Messages)
342		},
343		(ExchangeRequest::Event(ereq), ExchangeResponse::Event(eresp)) => {
344			verify_event(ereq, eresp).map(ExchangeVerified::Event)
345		},
346		_ => Err(VerifyError::VariantMismatch),
347	}
348}
349
350#[cfg(test)]
351mod tests {
352	use super::*;
353	use crate::{
354		lift::{
355			build_requires, ConsumptionRecord, Interval, LiftsBySource, MmrInclusionProof,
356			RequiresLift,
357		},
358		mmr::{MmrRoot, SpecMerge},
359		streams_root::{gen_stream_proof, streams_root, TreeStep},
360		test_utils::{ch, frontier_at, mmr_size},
361	};
362	use alloc::collections::BTreeMap;
363	use codec::Encode;
364	use mmr_lib::{
365		helper::get_peak_map,
366		leaf_index_to_mmr_size,
367		util::{MemMMR, MemStore},
368	};
369	use polkadot_parachain_primitives::primitives::Id as ParaId;
370	use sp_core::H256;
371
372	/// The stream root of an `mmr_lib` MMR: its bag bound to the leaf count.
373	fn bound(mmr_size: u64, bag: Hash) -> Hash {
374		MmrRoot::from_bag(get_peak_map(mmr_size), bag).0
375	}
376
377	/// The stream root over the first `n` leaves.
378	fn current_root(leaves: &[Hash], n: usize) -> Hash {
379		let store = MemStore::<Hash>::default();
380		let mut mmr = MemMMR::<Hash, SpecMerge>::new(0, &store);
381		for l in &leaves[..n] {
382			mmr.push(*l).unwrap();
383		}
384		bound(mmr.mmr_size(), mmr.get_root().unwrap())
385	}
386
387	/// O(log n) ancestry extension from `k` leaves to `n` leaves.
388	fn ancestry(leaves: &[Hash], k: usize, n: usize) -> MMRExtensionProof {
389		let store = MemStore::<Hash>::default();
390		let mut mmr = MemMMR::<Hash, SpecMerge>::new(0, &store);
391		for l in &leaves[..n] {
392			mmr.push(*l).unwrap();
393		}
394		let ap = mmr.gen_ancestry_proof(mmr_size(k)).unwrap();
395		MMRExtensionProof {
396			leaf_count: n as u64,
397			connecting_nodes: BoundedVec::truncate_from(
398				ap.prev_peaks_proof.proof_items().iter().map(|(_, h)| *h).collect::<Vec<_>>(),
399			),
400		}
401	}
402
403	/// A 6-message stream commits a `StreamsRoot`; a partial fetch (messages 2..3, extension 4→6)
404	/// verifies under it, and consuming the same messages lifts to the same root through
405	/// `build_requires`. Fetch and lift run the same `extension` + `tree_proof` check.
406	#[test]
407	fn composition_fetch_verify_then_build_requires() {
408		let source = ParaId::from(1000);
409		let stream = ch(2000);
410
411		let payloads: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i, i.wrapping_add(10)]).collect();
412		let leaves: Vec<Hash> = payloads.iter().map(|p| leaf_hash(0, p)).collect();
413
414		// Sender's current stream root (6 leaves) and the StreamsRoot committing it.
415		let r_current = current_root(&leaves, 6);
416		let under = streams_root(&BTreeMap::from([(stream, r_current)])).unwrap();
417		let (_r, tree_proof) =
418			gen_stream_proof(&BTreeMap::from([(stream, r_current)]), stream).unwrap();
419
420		// Fetch: base 2 (non-empty start_peaks), payloads [2,3] only → a real extension (4 → 6).
421		let resp = MessagesResponse {
422			base: MessagePosition(2),
423			leaf_version: 0,
424			payloads: vec![payloads[2].clone(), payloads[3].clone()].try_into().unwrap(),
425			start_peaks: BoundedVec::truncate_from(frontier_at(&leaves, 2).peaks().to_vec()),
426			extension: ancestry(&leaves, 4, 6),
427			tree_proof: tree_proof.clone(),
428		};
429		assert_eq!(verify_messages_response(stream, under, &resp), Ok(()));
430
431		// Requires: receiver consumed messages 2,3 (frontier 2 → 4); lift the endpoint (4 → 6).
432		let record = ConsumptionRecord {
433			entries: BTreeMap::from([(
434				source,
435				BTreeMap::from([(
436					stream,
437					Interval {
438						start: frontier_at(&leaves, 2).root(),
439						end: frontier_at(&leaves, 4),
440					},
441				)]),
442			)]),
443		};
444		let mut lifts = BTreeMap::new();
445		lifts.insert(
446			source,
447			vec![RequiresLift {
448				advances: Vec::new(),
449				extension: ancestry(&leaves, 4, 6),
450				tree_proof,
451			}],
452		);
453
454		let lifts = LiftsBySource::try_from(lifts).unwrap();
455		let requires = build_requires(&[record], &lifts).unwrap().expect("consumed, so a set");
456		// The fetch and the requires resolve to the SAME committed StreamsRoot.
457		assert_eq!(requires.get(source), Some(&under));
458	}
459
460	#[test]
461	fn leaf_hash_is_payload_only_and_version_disjoint() {
462		let payload = b"hello";
463		// Matches the explicit preimage `H(LEAF_TAG ++ version ++ payload)`.
464		let mut pre = Vec::new();
465		pre.push(LEAF_TAG);
466		pre.push(0u8);
467		pre.extend_from_slice(payload);
468		assert_eq!(leaf_hash(0, payload), <SpecHasher as HashT>::hash(&pre));
469		// Different version → different leaf (hash-disjoint domains).
470		assert_ne!(leaf_hash(0, payload), leaf_hash(1, payload));
471	}
472
473	#[test]
474	fn leaf_hash_frozen_vector() {
475		// FROZEN consensus vector: `H(0x01 ‖ 0x00 ‖ "hello")`. Any change to `LEAF_TAG`,
476		// `LEAF_VERSION`, the preimage layout or the hasher breaks this.
477		assert_eq!(
478			leaf_hash(crate::LEAF_VERSION, b"hello"),
479			Hash::from(hex_literal::hex!(
480				"cd31917fb8992dae762dbaaf276d8eb65aa89cdfb87daf69e05f8c08b490e78b"
481			))
482		);
483	}
484
485	/// A response for the whole stream from `base = 0` verifies under the committed root.
486	#[test]
487	fn messages_response_round_trips_under_streams_root() {
488		let payloads: Vec<Vec<u8>> = (0..4u8).map(|i| vec![i, i + 1, i + 2]).collect();
489
490		// Sender stream MMR over the payload leaves; its root is the stream root.
491		let store = MemStore::<Hash>::default();
492		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
493		for p in &payloads {
494			src.push(leaf_hash(0, p)).unwrap();
495		}
496		let stream_root = bound(src.mmr_size(), src.get_root().unwrap());
497
498		let stream = ch(2000);
499		let entries = BTreeMap::from([(stream, stream_root)]);
500		let under = streams_root(&entries).unwrap();
501		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
502
503		// Full stream from base 0: empty start_peaks, identity extension (payloads reach the root).
504		let resp = MessagesResponse {
505			base: MessagePosition(0),
506			leaf_version: 0,
507			payloads: payloads.clone().try_into().unwrap(),
508			start_peaks: BoundedVec::new(),
509			extension: MMRExtensionProof::identity(),
510			tree_proof,
511		};
512
513		assert_eq!(verify_messages_response(stream, under, &resp), Ok(()));
514		// Sanity: the recomputed root matches the sender's.
515		let mut mmr = MmrFrontier::new();
516		for p in &resp.payloads {
517			mmr.append(leaf_hash(0, p));
518		}
519		assert_eq!(mmr.root().0, stream_root);
520	}
521
522	#[test]
523	fn verify_messages_binds_base_to_request_start() {
524		let payloads: Vec<Vec<u8>> = (0..4u8).map(|i| vec![i, i + 1]).collect();
525		let store = MemStore::<Hash>::default();
526		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
527		for p in &payloads {
528			src.push(leaf_hash(0, p)).unwrap();
529		}
530		let stream = ch(2000);
531		let entries = BTreeMap::from([(stream, bound(src.mmr_size(), src.get_root().unwrap()))]);
532		let under = streams_root(&entries).unwrap();
533		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
534
535		// Full stream from base 0 (identity extension) verifies under `under`.
536		let resp = MessagesResponse {
537			base: MessagePosition(0),
538			leaf_version: 0,
539			payloads: payloads.clone().try_into().unwrap(),
540			start_peaks: BoundedVec::new(),
541			extension: MMRExtensionProof::identity(),
542			tree_proof,
543		};
544
545		// 4 payloads × 2 bytes = 8 bytes; a budget that comfortably fits them.
546		let total_bytes = payloads.iter().map(|p| p.len() as u32).sum::<u32>();
547
548		// start == base and within budget → dispatches and authenticates.
549		let req =
550			MessagesRequest { stream, start: MessagePosition(0), under, max_bytes: total_bytes };
551		assert_eq!(verify_messages(&req, &resp), Ok(()));
552
553		// start != base → rejected up front (before any crypto), and distinct from RootMismatch:
554		// the payloads do verify under `under`; only the answered range is wrong.
555		let req_wrong =
556			MessagesRequest { stream, start: MessagePosition(2), under, max_bytes: total_bytes };
557		assert_eq!(verify_messages(&req_wrong, &resp), Err(VerifyError::UnexpectedBase));
558
559		// Payloads exceed the byte budget → ExceedsBudget (not a proof/root failure).
560		let req_tight = MessagesRequest {
561			stream,
562			start: MessagePosition(0),
563			under,
564			max_bytes: total_bytes - 1,
565		};
566		assert_eq!(verify_messages(&req_tight, &resp), Err(VerifyError::ExceedsBudget));
567
568		// `max_bytes == 0` requests a payload-free (lift-material) proof, so any payload trips it.
569		let req_liftonly =
570			MessagesRequest { stream, start: MessagePosition(0), under, max_bytes: 0 };
571		assert_eq!(verify_messages(&req_liftonly, &resp), Err(VerifyError::ExceedsBudget));
572	}
573
574	#[test]
575	fn zero_budget_refuses_even_empty_payloads() {
576		// A stream of two empty payloads: they total 0 bytes, so only the "proofs only" rule
577		// refuses them under `max_bytes == 0`.
578		let payloads: Vec<Vec<u8>> = vec![Vec::new(), Vec::new()];
579		let stream = ch(2000);
580		let entries = BTreeMap::from([(
581			stream,
582			frontier_at(&payloads.iter().map(|p| leaf_hash(0, p)).collect::<Vec<_>>(), 2)
583				.root()
584				.0,
585		)]);
586		let under = streams_root(&entries).unwrap();
587		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
588		let resp = MessagesResponse {
589			base: MessagePosition(0),
590			leaf_version: 0,
591			payloads: payloads.clone().try_into().unwrap(),
592			start_peaks: BoundedVec::new(),
593			extension: MMRExtensionProof::identity(),
594			tree_proof,
595		};
596		let req =
597			|max_bytes| MessagesRequest { stream, start: MessagePosition(0), under, max_bytes };
598		assert_eq!(verify_messages(&req(1), &resp), Ok(()));
599		assert_eq!(verify_messages(&req(0), &resp), Err(VerifyError::ExceedsBudget));
600	}
601
602	#[test]
603	fn a_response_past_the_payload_cap_fails_to_decode() {
604		// Empty payloads cost one wire byte each and no budget, but each would be hashed: the
605		// count is bounded at the length prefix, before anything is allocated or hashed.
606		let cap = MAX_MESSAGES_PER_RESPONSE as usize;
607		let encoded = |count: usize| {
608			(
609				MessagePosition(0),
610				0u8,
611				vec![Vec::<u8>::new(); count],
612				Vec::<Hash>::new(),
613				MMRExtensionProof::identity(),
614				StreamProof { steps: Default::default() },
615			)
616				.encode()
617		};
618		assert!(MessagesResponse::decode(&mut &encoded(cap)[..]).is_ok());
619		assert!(MessagesResponse::decode(&mut &encoded(cap + 1)[..]).is_err());
620	}
621
622	#[test]
623	fn messages_response_rejects_wrong_root_or_tampered_payload() {
624		let payloads: Vec<Vec<u8>> = (0..3u8).map(|i| vec![i]).collect();
625		let store = MemStore::<Hash>::default();
626		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
627		for p in &payloads {
628			src.push(leaf_hash(0, p)).unwrap();
629		}
630		let stream = ch(2000);
631		let entries = BTreeMap::from([(stream, bound(src.mmr_size(), src.get_root().unwrap()))]);
632		let under = streams_root(&entries).unwrap();
633		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
634
635		let resp = MessagesResponse {
636			base: MessagePosition(0),
637			leaf_version: 0,
638			payloads: payloads.try_into().unwrap(),
639			start_peaks: BoundedVec::new(),
640			extension: MMRExtensionProof::identity(),
641			tree_proof,
642		};
643
644		// Wrong `under` → recomputed root doesn't match the requested one.
645		assert_eq!(
646			verify_messages_response(stream, StreamsRoot(H256::repeat_byte(0xff)), &resp),
647			Err(VerifyError::RootMismatch),
648		);
649		// Tampered payload → recomputed root diverges → also a mismatch against `under`.
650		let mut bad = resp.clone();
651		bad.payloads[0] = vec![0xAA];
652		assert_eq!(verify_messages_response(stream, under, &bad), Err(VerifyError::RootMismatch));
653	}
654
655	#[test]
656	fn malformed_frontier_rejects_without_panicking() {
657		let stream = ch(2000);
658		let under = StreamsRoot(H256::repeat_byte(0xff));
659
660		// `start_peaks` shape inconsistent with `base` (empty peaks, base = 1): the pre-fix
661		// pop-from-empty panic in `append`. `from_parts` must reject it cleanly, not panic.
662		let crafted = MessagesResponse {
663			base: MessagePosition(1),
664			leaf_version: 0,
665			payloads: vec![vec![0x01]].try_into().unwrap(),
666			start_peaks: BoundedVec::new(),
667			extension: MMRExtensionProof::identity(),
668			tree_proof: StreamProof { steps: Default::default() },
669		};
670		assert_eq!(
671			verify_messages_response(stream, under, &crafted),
672			Err(VerifyError::MalformedResponse),
673		);
674
675		// A `base` past the ceiling. One set bit means one peak passes the shape check, so the
676		// ceiling is what rejects.
677		let too_large = MessagesResponse {
678			base: MessagePosition(1 << 63),
679			leaf_version: 0,
680			payloads: vec![vec![0x01]].try_into().unwrap(),
681			start_peaks: BoundedVec::truncate_from(vec![H256::zero(); 1]),
682			extension: MMRExtensionProof::identity(),
683			tree_proof: StreamProof { steps: Default::default() },
684		};
685		assert_eq!(
686			verify_messages_response(stream, under, &too_large),
687			Err(VerifyError::MalformedResponse),
688		);
689
690		// A `base` at the ceiling is a valid frontier, but its next leaf would carry the count past
691		// it: the frontier would no longer decode. Rejected before anything is appended.
692		let at_ceiling = MessagesResponse {
693			base: MessagePosition(MAX_MMR_LEAF_COUNT),
694			leaf_version: 0,
695			payloads: vec![vec![0x01]].try_into().unwrap(),
696			start_peaks: BoundedVec::truncate_from(vec![H256::zero(); 1]),
697			extension: MMRExtensionProof::identity(),
698			tree_proof: StreamProof { steps: Default::default() },
699		};
700		assert_eq!(
701			verify_messages_response(stream, under, &at_ceiling),
702			Err(VerifyError::MalformedResponse),
703		);
704
705		// `base = u64::MAX` (64 set bits, 64 peaks) once reached the `base + payloads` counter
706		// guard; `from_parts`'s ceiling now rejects it first. Kept so both stay covered.
707		let overflow = MessagesResponse {
708			base: MessagePosition(u64::MAX),
709			leaf_version: 0,
710			payloads: vec![vec![0x01]].try_into().unwrap(),
711			start_peaks: BoundedVec::truncate_from(vec![H256::zero(); MAX_MMR_HASHES as usize]),
712			extension: MMRExtensionProof::identity(),
713			tree_proof: StreamProof { steps: Default::default() },
714		};
715		assert_eq!(
716			verify_messages_response(stream, under, &overflow),
717			Err(VerifyError::MalformedResponse),
718		);
719	}
720
721	/// The exchange envelope's variant indices are wire-format-critical: foreign implementations
722	/// decode by them, forever. Messages = 0x00, Event = 0x01 on both request and response.
723	#[test]
724	fn exchange_envelope_indices_are_frozen() {
725		let mreq = MessagesRequest {
726			stream: ch(1),
727			start: MessagePosition(0),
728			under: StreamsRoot(H256::zero()),
729			max_bytes: 0,
730		};
731		let ereq = EventRequest { stream: ch(1), under: StreamsRoot(H256::zero()), at: None };
732		assert_eq!(ExchangeRequest::Messages(mreq).encode()[0], 0x00);
733		assert_eq!(ExchangeRequest::Event(ereq).encode()[0], 0x01);
734
735		let mresp = MessagesResponse {
736			base: MessagePosition(0),
737			leaf_version: 0,
738			payloads: Vec::new().try_into().unwrap(),
739			start_peaks: BoundedVec::new(),
740			extension: MMRExtensionProof::identity(),
741			tree_proof: StreamProof { steps: Default::default() },
742		};
743		let eresp = EventResponse {
744			payload: Vec::new(),
745			leaf_version: 0,
746			inclusion: MmrInclusionProof { mmr_size: 0, items: BoundedVec::new() },
747			tree_proof: StreamProof { steps: Default::default() },
748		};
749		assert_eq!(ExchangeResponse::Messages(mresp).encode()[0], 0x00);
750		assert_eq!(ExchangeResponse::Event(eresp).encode()[0], 0x01);
751	}
752
753	/// The head event verifies under the committed root; a wrong root or payload does not.
754	#[test]
755	fn event_response_round_trips_under_streams_root() {
756		let stream = ch(2000);
757		// 6 leaves: the head (index 5) has a non-trivial sibling path AND other peaks, exercising
758		// both halves of `verify_head`'s item split.
759		let payloads: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i, i.wrapping_add(7)]).collect();
760
761		let store = MemStore::<Hash>::default();
762		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
763		let positions: Vec<u64> =
764			payloads.iter().map(|p| src.push(leaf_hash(0, p)).unwrap()).collect();
765		let stream_root = bound(src.mmr_size(), src.get_root().unwrap());
766
767		let entries = BTreeMap::from([(stream, stream_root)]);
768		let under = streams_root(&entries).unwrap();
769		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
770
771		// Inclusion proof for the head leaf (index 5).
772		let head = payloads.len() - 1;
773		let mproof = src.gen_proof(vec![positions[head]]).unwrap();
774		let inclusion = MmrInclusionProof {
775			mmr_size: mmr_size(payloads.len()),
776			items: BoundedVec::truncate_from(mproof.proof_items().to_vec()),
777		};
778
779		let resp = EventResponse {
780			payload: payloads[head].clone(),
781			leaf_version: 0,
782			inclusion,
783			tree_proof,
784		};
785
786		// Head verify returns the frontier too (needed as the lossy-consumption interval endpoint).
787		let (pos, frontier) = verify_event_response(stream, under, &resp).expect("head verifies");
788		assert_eq!(pos, MessagePosition(head as u64));
789		assert_eq!(frontier.leaf_count(), 6);
790		assert_eq!(frontier.root().0, stream_root);
791
792		// The request-aware dispatcher (`at = None`) yields the same head result.
793		let req = EventRequest { stream, under, at: None };
794		assert_eq!(
795			verify_event(&req, &resp),
796			Ok(VerifiedEvent::Head { position: MessagePosition(head as u64), frontier }),
797		);
798
799		// A head proof used *positionally* at a non-head index → rejected (the proof's shape
800		// doesn't match that position; exact error is mmr_lib-dependent).
801		assert!(verify_positional_event_response(stream, under, MessagePosition(0), &resp).is_err());
802		// Wrong `under` → the recomputed root doesn't match the requested one.
803		assert_eq!(
804			verify_event_response(stream, StreamsRoot(H256::repeat_byte(0xff)), &resp),
805			Err(VerifyError::RootMismatch),
806		);
807		// Tampered payload → verify_head succeeds structurally but derives a diverging root.
808		let mut bad = resp.clone();
809		bad.payload = vec![0xAA];
810		assert_eq!(verify_event_response(stream, under, &bad), Err(VerifyError::RootMismatch));
811		// Oversized payload → rejected before hashing.
812		let mut big = resp.clone();
813		big.payload = vec![0u8; MAX_SPECULATIVE_MESSAGE_LEN as usize + 1];
814		assert_eq!(verify_event_response(stream, under, &big), Err(VerifyError::PayloadTooLarge));
815		// At the bound the size check passes; the forged payload then fails the proof.
816		big.payload.pop();
817		assert_eq!(verify_event_response(stream, under, &big), Err(VerifyError::RootMismatch));
818
819		// Structurally invalid inclusion proof (invalid mmr_size) → surfaced as `Proof(..)`.
820		let mut bogus = resp.clone();
821		bogus.inclusion.mmr_size = 2;
822		assert_eq!(
823			verify_event_response(stream, under, &bogus),
824			Err(VerifyError::Proof(ProofError::InvalidProof)),
825		);
826	}
827
828	#[test]
829	fn event_response_rejects_a_forged_head_position() {
830		// The two peaks of 3 leaves also form a frontier of `2^47 + 1` leaves, and bag identically.
831		// A server claiming that size in `mmr_size` must not pass the head off at position `2^47`.
832		let stream = ch(2000);
833		let payloads: Vec<Vec<u8>> = (0..3u8).map(|i| vec![i]).collect();
834		let leaves: Vec<Hash> = payloads.iter().map(|p| leaf_hash(0, p)).collect();
835		let entries = BTreeMap::from([(stream, frontier_at(&leaves, 3).root().0)]);
836		let under = streams_root(&entries).unwrap();
837		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
838
839		let forged_count = (1u64 << 47) + 1;
840		let resp = EventResponse {
841			payload: payloads[2].clone(),
842			leaf_version: 0,
843			inclusion: MmrInclusionProof {
844				mmr_size: leaf_index_to_mmr_size(forged_count - 1),
845				items: BoundedVec::truncate_from(frontier_at(&leaves, 2).peaks().to_vec()),
846			},
847			tree_proof,
848		};
849		assert_eq!(verify_event_response(stream, under, &resp), Err(VerifyError::RootMismatch));
850	}
851
852	#[test]
853	fn positional_event_response_round_trips_under_streams_root() {
854		let stream = ch(2000);
855		let payloads: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i, i.wrapping_add(7)]).collect();
856
857		let store = MemStore::<Hash>::default();
858		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
859		let positions: Vec<u64> =
860			payloads.iter().map(|p| src.push(leaf_hash(0, p)).unwrap()).collect();
861		let stream_root = bound(src.mmr_size(), src.get_root().unwrap());
862
863		let entries = BTreeMap::from([(stream, stream_root)]);
864		let under = streams_root(&entries).unwrap();
865		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
866
867		// Inclusion proof for a non-head leaf (index 2): the positional read path.
868		let at = 2usize;
869		let mproof = src.gen_proof(vec![positions[at]]).unwrap();
870		let inclusion = MmrInclusionProof {
871			mmr_size: mmr_size(payloads.len()),
872			items: BoundedVec::truncate_from(mproof.proof_items().to_vec()),
873		};
874
875		let resp =
876			EventResponse { payload: payloads[at].clone(), leaf_version: 0, inclusion, tree_proof };
877
878		// Correct position → payload authenticated under `under`.
879		assert_eq!(
880			verify_positional_event_response(stream, under, MessagePosition(at as u64), &resp),
881			Ok(()),
882		);
883		// Wrong position (same proof) → rejected (exact error is mmr_lib-dependent).
884		assert!(verify_positional_event_response(stream, under, MessagePosition(3), &resp).is_err());
885		// Wrong `under` → the recomputed root doesn't match the requested one.
886		assert_eq!(
887			verify_positional_event_response(
888				stream,
889				StreamsRoot(H256::repeat_byte(0xff)),
890				MessagePosition(at as u64),
891				&resp,
892			),
893			Err(VerifyError::RootMismatch),
894		);
895		// Tampered payload → verify_leaf succeeds structurally but derives a diverging root.
896		let mut bad = resp.clone();
897		bad.payload = vec![0xAA];
898		assert_eq!(
899			verify_positional_event_response(stream, under, MessagePosition(at as u64), &bad),
900			Err(VerifyError::RootMismatch),
901		);
902
903		// The request-aware dispatcher (`at = Some(2)`) yields the positional result.
904		let req = EventRequest { stream, under, at: Some(MessagePosition(at as u64)) };
905		assert_eq!(
906			verify_event(&req, &resp),
907			Ok(VerifiedEvent::Positional { position: MessagePosition(at as u64) }),
908		);
909	}
910
911	#[test]
912	fn event_response_tree_proof_failure_surfaces_as_treeproof() {
913		// A split bit past the key width fails the tree walk (`TreeProof`, not `RootMismatch`).
914		let stream = ch(2000);
915		let payloads: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i]).collect();
916		let store = MemStore::<Hash>::default();
917		let mut src = MemMMR::<Hash, SpecMerge>::new(0, &store);
918		let positions: Vec<u64> =
919			payloads.iter().map(|p| src.push(leaf_hash(0, p)).unwrap()).collect();
920		let head = payloads.len() - 1;
921		let mproof = src.gen_proof(vec![positions[head]]).unwrap();
922		let resp = EventResponse {
923			payload: payloads[head].clone(),
924			leaf_version: 0,
925			inclusion: MmrInclusionProof {
926				mmr_size: mmr_size(payloads.len()),
927				items: BoundedVec::truncate_from(mproof.proof_items().to_vec()),
928			},
929			// `split_bit = 255` is >= KEY_BITS, so `streams_root_from_proof` rejects the walk.
930			tree_proof: StreamProof {
931				steps: vec![TreeStep { split_bit: 255, sibling: H256::zero() }].try_into().unwrap(),
932			},
933		};
934		assert_eq!(
935			verify_event_response(stream, StreamsRoot(H256::repeat_byte(0xff)), &resp),
936			Err(VerifyError::TreeProof),
937		);
938	}
939
940	#[test]
941	fn verify_exchange_dispatches_and_binds_variant() {
942		// A valid Messages request/response pair.
943		let mpayloads: Vec<Vec<u8>> = (0..3u8).map(|i| vec![i]).collect();
944		let mstore = MemStore::<Hash>::default();
945		let mut msrc = MemMMR::<Hash, SpecMerge>::new(0, &mstore);
946		for p in &mpayloads {
947			msrc.push(leaf_hash(0, p)).unwrap();
948		}
949		let mstream = ch(2000);
950		let mentries =
951			BTreeMap::from([(mstream, bound(msrc.mmr_size(), msrc.get_root().unwrap()))]);
952		let munder = streams_root(&mentries).unwrap();
953		let (_r, mtree) = gen_stream_proof(&mentries, mstream).unwrap();
954		let mresp = MessagesResponse {
955			base: MessagePosition(0),
956			leaf_version: 0,
957			payloads: mpayloads.clone().try_into().unwrap(),
958			start_peaks: BoundedVec::new(),
959			extension: MMRExtensionProof::identity(),
960			tree_proof: mtree,
961		};
962		let mbytes = mpayloads.iter().map(|p| p.len() as u32).sum::<u32>();
963		let mreq = MessagesRequest {
964			stream: mstream,
965			start: MessagePosition(0),
966			under: munder,
967			max_bytes: mbytes,
968		};
969
970		// A valid Event (head) request/response pair.
971		let epayloads: Vec<Vec<u8>> = (0..6u8).map(|i| vec![i, i + 1]).collect();
972		let estore = MemStore::<Hash>::default();
973		let mut esrc = MemMMR::<Hash, SpecMerge>::new(0, &estore);
974		let epos: Vec<u64> =
975			epayloads.iter().map(|p| esrc.push(leaf_hash(0, p)).unwrap()).collect();
976		let estream = ch(3000);
977		let eentries =
978			BTreeMap::from([(estream, bound(esrc.mmr_size(), esrc.get_root().unwrap()))]);
979		let eunder = streams_root(&eentries).unwrap();
980		let (_r2, etree) = gen_stream_proof(&eentries, estream).unwrap();
981		let ehead = epayloads.len() - 1;
982		let eproof = esrc.gen_proof(vec![epos[ehead]]).unwrap();
983		let eresp = EventResponse {
984			payload: epayloads[ehead].clone(),
985			leaf_version: 0,
986			inclusion: MmrInclusionProof {
987				mmr_size: mmr_size(epayloads.len()),
988				items: BoundedVec::truncate_from(eproof.proof_items().to_vec()),
989			},
990			tree_proof: etree,
991		};
992		let ereq = EventRequest { stream: estream, under: eunder, at: None };
993
994		// Matching variants → dispatched and authenticated.
995		assert_eq!(
996			verify_exchange(
997				&ExchangeRequest::Messages(mreq.clone()),
998				&ExchangeResponse::Messages(mresp.clone()),
999			),
1000			Ok(ExchangeVerified::Messages),
1001		);
1002		assert!(matches!(
1003			verify_exchange(
1004				&ExchangeRequest::Event(ereq.clone()),
1005				&ExchangeResponse::Event(eresp.clone()),
1006			),
1007			Ok(ExchangeVerified::Event(VerifiedEvent::Head { .. })),
1008		));
1009
1010		// Cross-variant answers → VariantMismatch, before any crypto.
1011		assert_eq!(
1012			verify_exchange(&ExchangeRequest::Messages(mreq), &ExchangeResponse::Event(eresp)),
1013			Err(VerifyError::VariantMismatch),
1014		);
1015		assert_eq!(
1016			verify_exchange(&ExchangeRequest::Event(ereq), &ExchangeResponse::Messages(mresp)),
1017			Err(VerifyError::VariantMismatch),
1018		);
1019	}
1020
1021	/// Two committed 6-leaf streams with distinct payloads, and the `StreamsRoot` over both.
1022	struct TwoStreams {
1023		under: StreamsRoot,
1024		/// Per stream: payloads, leaves, tree proof.
1025		streams: [(StreamId, Vec<Vec<u8>>, Vec<Hash>, StreamProof); 2],
1026	}
1027
1028	fn two_streams() -> TwoStreams {
1029		let ids = [ch(2000), ch(3000)];
1030		let payloads: [Vec<Vec<u8>>; 2] =
1031			core::array::from_fn(|s| (0..6u8).map(|i| vec![s as u8, i]).collect());
1032		let leaves: [Vec<Hash>; 2] =
1033			core::array::from_fn(|s| payloads[s].iter().map(|p| leaf_hash(0, p)).collect());
1034		let entries: BTreeMap<_, _> =
1035			(0..2).map(|s| (ids[s], frontier_at(&leaves[s], 6).root().0)).collect();
1036		let under = streams_root(&entries).unwrap();
1037		let streams = core::array::from_fn(|s| {
1038			let (_, proof) = gen_stream_proof(&entries, ids[s]).unwrap();
1039			(ids[s], payloads[s].clone(), leaves[s].clone(), proof)
1040		});
1041		TwoStreams { under, streams }
1042	}
1043
1044	/// The honest fetch of `payloads[base..end]` with a real extension from `end` to 6.
1045	fn fetch(
1046		(_, payloads, leaves, tree_proof): &(StreamId, Vec<Vec<u8>>, Vec<Hash>, StreamProof),
1047		base: usize,
1048		end: usize,
1049	) -> MessagesResponse {
1050		MessagesResponse {
1051			base: MessagePosition(base as u64),
1052			leaf_version: 0,
1053			payloads: payloads[base..end].to_vec().try_into().unwrap(),
1054			start_peaks: BoundedVec::truncate_from(frontier_at(leaves, base).peaks().to_vec()),
1055			extension: ancestry(leaves, end, 6),
1056			tree_proof: tree_proof.clone(),
1057		}
1058	}
1059
1060	/// The honest head read of a 6-leaf stream.
1061	fn head(
1062		(_, payloads, leaves, tree_proof): &(StreamId, Vec<Vec<u8>>, Vec<Hash>, StreamProof),
1063	) -> EventResponse {
1064		let store = MemStore::<Hash>::default();
1065		let mut mmr = MemMMR::<Hash, SpecMerge>::new(0, &store);
1066		let positions: Vec<u64> = leaves.iter().map(|l| mmr.push(*l).unwrap()).collect();
1067		let proof = mmr.gen_proof(vec![positions[5]]).unwrap();
1068		EventResponse {
1069			payload: payloads[5].clone(),
1070			leaf_version: 0,
1071			inclusion: MmrInclusionProof {
1072				mmr_size: mmr_size(6),
1073				items: BoundedVec::truncate_from(proof.proof_items().to_vec()),
1074			},
1075			tree_proof: tree_proof.clone(),
1076		}
1077	}
1078
1079	#[test]
1080	fn a_forged_tail_after_a_genuine_prefix_does_not_verify() {
1081		let t = two_streams();
1082		let (stream, ..) = t.streams[0];
1083		let honest = fetch(&t.streams[0], 2, 4);
1084		assert_eq!(verify_messages_response(stream, t.under, &honest), Ok(()));
1085
1086		// Leaf 2 is genuine and leaf 3 forged; the extension from 4 to 6 is the real one.
1087		let mut forged = honest.clone();
1088		forged.payloads[1] = vec![0xAA];
1089		assert_eq!(
1090			verify_messages_response(stream, t.under, &forged),
1091			Err(VerifyError::RootMismatch)
1092		);
1093
1094		// A forged leaf appended past the genuine ones, with the real extension from 5 to 6.
1095		let mut appended = fetch(&t.streams[0], 2, 4);
1096		appended.payloads.try_push(vec![0xAA]).unwrap();
1097		appended.extension = ancestry(&t.streams[0].2, 5, 6);
1098		assert_eq!(
1099			verify_messages_response(stream, t.under, &appended),
1100			Err(VerifyError::RootMismatch)
1101		);
1102	}
1103
1104	#[test]
1105	fn a_messages_payload_past_the_bound_is_too_large() {
1106		let t = two_streams();
1107		let (stream, ..) = t.streams[0];
1108		let mut resp = fetch(&t.streams[0], 2, 4);
1109		resp.payloads[1] = vec![0; MAX_SPECULATIVE_MESSAGE_LEN as usize + 1];
1110		let req = MessagesRequest {
1111			stream,
1112			start: MessagePosition(2),
1113			under: t.under,
1114			max_bytes: u32::MAX,
1115		};
1116		assert_eq!(verify_messages(&req, &resp), Err(VerifyError::PayloadTooLarge));
1117
1118		// At the bound the size check passes; the forged payload then fails the proof.
1119		resp.payloads[1].pop();
1120		assert_eq!(verify_messages(&req, &resp), Err(VerifyError::RootMismatch));
1121	}
1122
1123	#[test]
1124	fn a_wrong_leaf_version_is_a_root_mismatch() {
1125		let t = two_streams();
1126		let (stream, ..) = t.streams[0];
1127		let mut resp = fetch(&t.streams[0], 2, 4);
1128		resp.leaf_version = 1;
1129		assert_eq!(
1130			verify_messages_response(stream, t.under, &resp),
1131			Err(VerifyError::RootMismatch)
1132		);
1133
1134		let mut event = head(&t.streams[0]);
1135		event.leaf_version = 1;
1136		assert_eq!(verify_event_response(stream, t.under, &event), Err(VerifyError::RootMismatch));
1137	}
1138
1139	#[test]
1140	fn material_of_another_stream_is_a_root_mismatch() {
1141		let t = two_streams();
1142		let (a, ..) = t.streams[0];
1143		let (b, ..) = t.streams[1];
1144
1145		// Stream A's payloads and MMR proofs with stream B's tree proof.
1146		let mut resp = fetch(&t.streams[0], 2, 4);
1147		resp.tree_proof = t.streams[1].3.clone();
1148		assert_eq!(verify_messages_response(a, t.under, &resp), Err(VerifyError::RootMismatch));
1149
1150		// Stream B's whole response, offered as stream A.
1151		let resp = fetch(&t.streams[1], 2, 4);
1152		assert_eq!(verify_messages_response(b, t.under, &resp), Ok(()));
1153		assert_eq!(verify_messages_response(a, t.under, &resp), Err(VerifyError::RootMismatch));
1154
1155		let event = head(&t.streams[1]);
1156		assert!(verify_event_response(b, t.under, &event).is_ok());
1157		assert_eq!(verify_event_response(a, t.under, &event), Err(VerifyError::RootMismatch));
1158	}
1159
1160	#[test]
1161	fn a_response_without_payloads_verifies_and_advances_nothing() {
1162		// Valid lift material, so a server can always answer this way; the consumer's frontier
1163		// stays at `base`, and it must fetch again.
1164		let t = two_streams();
1165		let (stream, ..) = t.streams[0];
1166		let resp = fetch(&t.streams[0], 2, 2);
1167		assert!(resp.payloads.is_empty());
1168		for max_bytes in [0, u32::MAX] {
1169			let req =
1170				MessagesRequest { stream, start: MessagePosition(2), under: t.under, max_bytes };
1171			assert_eq!(verify_messages(&req, &resp), Ok(()));
1172		}
1173	}
1174}