referrerpolicy=no-referrer-when-downgrade

cumulus_primitives_spec_messaging/
lift.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//! Requires-lift and consumption-record primitives.
18//!
19//! Blocks never emit `Requires` and never see a `StreamsRoot`. Processing the messaging inherent
20//! writes a [`ConsumptionRecord`]: which streams the block touched and the [`Interval`] consumed on
21//! each. The `validate_block` wrapper then synthesizes the candidate's `Requires` set from the
22//! records plus PoV-carried lifts ([`RequiresLift`]), one per recorded stream, binding that
23//! stream's consumption to a current committed `StreamsRoot`. The relay chain sees only entries it
24//! can match.
25//!
26//! The invariant [`build_requires`] enforces, per stream and candidate: the recorded intervals form
27//! a proven forward chain (each block continues where the previous ended, or a PoV proof shows the
28//! gap is a forward extension, see [`stitch`]), and one lift binds the chain's endpoint to a
29//! committed root, which binds everything consumed.
30//!
31//! The record is STF output and trusted; the lifts are PoV data and verified here. Lifts are
32//! matched to a source's `StreamId`-sorted record streams by position. A mispaired lift cannot
33//! verify, because the `tree_proof` walk binds the record's key
34//! ([`crate::streams_root::streams_root_from_proof`]).
35
36use alloc::{collections::BTreeMap, vec, vec::Vec};
37use codec::{Decode, DecodeWithMemTracking, Encode};
38use mmr_lib::{
39	helper::{get_peak_map, get_peaks, is_valid_mmr_size},
40	leaf_index_to_pos, MerkleProof, NodeMerkleProof,
41};
42use polkadot_core_primitives::Hash;
43use polkadot_parachain_primitives::primitives::Id as ParaId;
44use polkadot_primitives::{v9::MAX_COMMITMENT_ENTRIES, RequiresSet};
45use scale_info::TypeInfo;
46use sp_core::ConstU32;
47use sp_runtime::BoundedVec;
48
49use crate::{
50	mmr::{
51		mmr_size, MessagePosition, MmrFrontier, MmrRoot, SpecMerge, MAX_MMR_HASHES,
52		MAX_MMR_LEAF_COUNT,
53	},
54	stream::StreamId,
55	streams_root::{streams_root_from_proof, StreamProof, StreamsRoot},
56};
57
58/// The keyed-tree proof a lift walks from a stream root to the `StreamsRoot`.
59pub type TreeInclusionProof = StreamProof;
60
61/// Upper bound on an [`MmrInclusionProof`]'s items. A single-leaf proof needs at most the subtree
62/// height plus the other peaks, each at most 63; this constant is a ceiling no valid proof reaches.
63/// Enforced at decode, which bounds `calculate_root`'s work on untrusted input.
64pub const MAX_INCLUSION_PROOF_ITEMS: u32 = 2 * 64;
65
66/// Why an MMR proof verification rejected its input. Typed so callers can tell a malformed or
67/// forged proof from an out-of-range or non-forward one.
68#[derive(Clone, Copy, PartialEq, Eq, Debug)]
69pub enum ProofError {
70	/// The proof is structurally malformed: an invalid `mmr_size`, a wrong item/node count, or
71	/// `mmr_lib` could not reconstruct a root from it.
72	InvalidProof,
73	/// A leaf position is at or past the proven MMR's leaf count (an empty MMR has no head).
74	PositionOutOfRange,
75	/// An extension does not go strictly forward (its target is not newer than the frontier).
76	NotForward,
77	/// A leaf count exceeds [`MAX_MMR_LEAF_COUNT`].
78	LeafCountTooLarge,
79}
80
81/// An append-only MMR extension proof: O(log n) connecting nodes that, with a frontier's own peaks,
82/// reconstruct a newer root. The `mmr_lib` `gen_ancestry_proof` form of the design's
83/// `connecting_nodes`. Verification computes the new root; it is never declared alongside the
84/// proof. It holds node hashes only, no message payloads.
85#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo, Default)]
86pub struct MMRExtensionProof {
87	/// Leaf count of the extended MMR. The `mmr_lib` node count is derived from it, so no
88	/// independent size can be smuggled in. `0` with empty `connecting_nodes` is the identity
89	/// extension.
90	/// Compact-encoded, since this is PoV-carried.
91	#[codec(compact)]
92	pub leaf_count: u64,
93	/// The connecting-node hashes, in `mmr_lib`'s proof order. Positions are not stored: an MMR's
94	/// shape is fixed by its size, so they are derived at verify time from the two leaf counts
95	/// (`ancestry_proof_positions`). Empty for the identity. At most [`MAX_MMR_HASHES`], enforced
96	/// at decode.
97	pub connecting_nodes: BoundedVec<Hash, ConstU32<MAX_MMR_HASHES>>,
98}
99
100impl MMRExtensionProof {
101	/// The identity extension. Yields a frontier's own root for the caught-up case.
102	pub fn identity() -> Self {
103		Self { leaf_count: 0, connecting_nodes: BoundedVec::new() }
104	}
105
106	/// Extend `from` and return the new root, computed from `from`'s peaks and the connecting
107	/// nodes; `Err` if the proof does not fit that placement. The identity extension yields
108	/// `from`'s own root. Comparing the root is the caller's job. `from`'s contents are not
109	/// re-checked: a forged peak yields a root no committed `StreamsRoot` can bind.
110	pub fn calculate_root(&self, from: &MmrFrontier) -> Result<MmrRoot, ProofError> {
111		// `mmr_lib` accepts counts up to 2^63; bound the proof's to `MAX_MMR_LEAF_COUNT` first.
112		// `from` is already in range by construction.
113		if self.leaf_count > MAX_MMR_LEAF_COUNT {
114			return Err(ProofError::LeafCountTooLarge);
115		}
116		// `0` is the identity only without connecting nodes. This also keeps the `leaf_count - 1`
117		// below from underflowing.
118		if self.leaf_count == 0 {
119			return if self.connecting_nodes.is_empty() {
120				Ok(from.root())
121			} else {
122				Err(ProofError::InvalidProof)
123			};
124		}
125		// Strictly forward: the extended MMR must have more leaves than the frontier.
126		if self.leaf_count <= from.leaf_count() {
127			return Err(ProofError::NotForward);
128		}
129		let new_mmr_size = mmr_size(self.leaf_count);
130
131		// Extending from the empty stream: nothing binds, so the connecting nodes must be the new
132		// MMR's peaks in `get_peaks` order.
133		if from.leaf_count() == 0 {
134			// The nodes must form a frontier of exactly `self.leaf_count` leaves; `from_parts`
135			// checks that.
136			return MmrFrontier::from_parts(self.connecting_nodes.to_vec(), self.leaf_count)
137				.map(|f| f.root())
138				.ok_or(ProofError::InvalidProof);
139		}
140
141		// `from`'s peaks are complete subtrees of the new MMR at unchanged positions; with the
142		// connecting nodes they reconstruct the new root under `SpecMerge`.
143		let old_mmr_size = from.mmr_size();
144		let old_positions = get_peaks(old_mmr_size);
145		// Positions are not carried: derive them from the two sizes and pair them with the hashes
146		// in `mmr_lib`'s proof order. A length mismatch means the proof does not fit the claimed
147		// extension.
148		let positions =
149			mmr_lib::ancestry_proof::ancestry_proof_positions(old_mmr_size, new_mmr_size)
150				.map_err(|_| ProofError::InvalidProof)?;
151		if positions.len() != self.connecting_nodes.len() {
152			return Err(ProofError::InvalidProof);
153		}
154		let proof: Vec<(u64, Hash)> =
155			positions.into_iter().zip(self.connecting_nodes.iter().copied()).collect();
156		let nodes: Vec<(u64, Hash)> =
157			old_positions.into_iter().zip(from.peaks().iter().copied()).collect();
158		NodeMerkleProof::<Hash, SpecMerge>::new(new_mmr_size, proof)
159			.calculate_root(nodes)
160			.map(|bag| MmrRoot::from_bag(self.leaf_count, bag))
161			.map_err(|_| ProofError::InvalidProof)
162	}
163}
164
165/// A single-leaf MMR inclusion proof: `mmr_lib`'s [`MerkleProof`] items (the sibling path plus the
166/// other peaks) and the node count it was generated against. Used by the event and register head
167/// read. Where [`MMRExtensionProof`] moves a frontier forward, this places one leaf under a fixed
168/// MMR. The root is derived, never declared.
169#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
170pub struct MmrInclusionProof {
171	/// `mmr_lib` node count of the MMR the proof was generated against. With the leaf count it
172	/// derives (`get_peak_map`), it pins a head read.
173	pub mmr_size: u64,
174	/// The leaf's sibling path plus the other peaks, in `mmr_lib` order.
175	pub items: BoundedVec<Hash, ConstU32<MAX_INCLUSION_PROOF_ITEMS>>,
176}
177
178impl MmrInclusionProof {
179	/// Leaf count of the proven MMR. `mmr_lib` accepts counts up to 2^63; ours is lower.
180	fn checked_leaf_count(&self) -> Result<u64, ProofError> {
181		if !is_valid_mmr_size(self.mmr_size) {
182			return Err(ProofError::InvalidProof);
183		}
184		let leaf_count = get_peak_map(self.mmr_size);
185		if leaf_count > MAX_MMR_LEAF_COUNT {
186			return Err(ProofError::LeafCountTooLarge);
187		}
188		Ok(leaf_count)
189	}
190
191	/// Verify `leaf` (already hashed by [`crate::message::leaf_hash`]) as the last leaf of the
192	/// proven MMR. Returns the head position and the frontier of that MMR; `Err` on any structural
193	/// defect.
194	///
195	/// The expected shape is what `gen_proof` produces for the last leaf: the other peaks left to
196	/// right, then the head's left siblings bottom up. The item count
197	/// is checked exactly.
198	///
199	/// Only reconstructs a frontier; does not bind it to any committed `StreamsRoot`. `Ok` is not
200	/// authentication. Prefer [`crate::message::verify_event`], which binds the result to a trusted
201	/// `under`.
202	pub fn verify_head(&self, leaf: Hash) -> Result<(MessagePosition, MmrFrontier), ProofError> {
203		let leaf_count = self.checked_leaf_count()?;
204		if leaf_count == 0 {
205			return Err(ProofError::PositionOutOfRange);
206		}
207		if self.items.len() != leaf_proof_len(leaf_count, leaf_count - 1) {
208			return Err(ProofError::InvalidProof);
209		}
210
211		let other_peaks = leaf_count.count_ones() as usize - 1;
212		let (peak_items, path) = self.items.split_at(other_peaks);
213		let mut last_peak = leaf;
214		for sibling in path {
215			last_peak = <SpecMerge as mmr_lib::Merge>::merge(sibling, &last_peak)
216				.expect("SpecMerge::merge is infallible; qed");
217		}
218		let peaks: Vec<Hash> =
219			peak_items.iter().copied().chain(core::iter::once(last_peak)).collect();
220		// `other_peaks + 1 == leaf_count.count_ones()` and the ceiling was checked, so this is
221		// `Some`.
222		let frontier =
223			MmrFrontier::from_parts(peaks, leaf_count).ok_or(ProofError::InvalidProof)?;
224
225		Ok((MessagePosition(leaf_count - 1), frontier))
226	}
227
228	/// Verify `leaf` (already hashed by [`crate::message::leaf_hash`]) at `position` and return the
229	/// implied [`MmrRoot`]. `Err` if the position is out of range or the proof malformed. Only
230	/// reconstructs the root; `Ok` is not authentication. Prefer [`crate::message::verify_event`],
231	/// which binds the result to a trusted `under`.
232	pub fn verify_leaf(
233		&self,
234		position: MessagePosition,
235		leaf: Hash,
236	) -> Result<MmrRoot, ProofError> {
237		let leaf_count = self.checked_leaf_count()?;
238		if position.0 >= leaf_count {
239			return Err(ProofError::PositionOutOfRange);
240		}
241		// `calculate_root` ignores the items of a 1-leaf proof, so padding would slip through.
242		if self.items.len() != leaf_proof_len(leaf_count, position.0) {
243			return Err(ProofError::InvalidProof);
244		}
245		let pos = leaf_index_to_pos(position.0);
246		MerkleProof::<Hash, SpecMerge>::new(self.mmr_size, self.items.to_vec())
247			.calculate_root(vec![(pos, leaf)])
248			.map(|bag| MmrRoot::from_bag(leaf_count, bag))
249			.map_err(|_| ProofError::InvalidProof)
250	}
251}
252
253/// Proof length `gen_proof` produces for leaf `index`: siblings up to its peak, the peaks to its
254/// left, and one bagged item for any peaks to its right.
255fn leaf_proof_len(leaf_count: u64, index: u64) -> usize {
256	let mut first = 0;
257	let heights = (0..u64::BITS).rev().filter(|h| leaf_count >> h & 1 == 1);
258	for (left_peaks, height) in heights.enumerate() {
259		let peak_leaves = 1u64 << height;
260		if index < first + peak_leaves {
261			let right_peaks = leaf_count.count_ones() as usize - left_peaks - 1;
262			return height as usize + left_peaks + usize::from(right_peaks > 0);
263		}
264		first += peak_leaves;
265	}
266	unreachable!("index < leaf_count lies under some peak; qed")
267}
268
269/// Per stream and block: consumption entered the block at `start` and left it at `end`.
270///
271/// The candidate's lift binds only the last state to a committed root; the intervals extend that
272/// guarantee back over the bundle. Each block must start where the previous ended, or prove the
273/// jump moved forward ([`stitch`]).
274///
275/// - Channels: `start` is the frontier root before the block's incoming messages, `end` the
276/// frontier after. `start == previous end` holds by construction, so the chain check is free.
277/// - Register and event reads: `end` is the context the reads were verified against, `start` its
278/// root; nothing advances. Contexts can jump between blocks, so a fabricated context breaks the
279/// chain instead of hiding behind a later genuine one.
280///
281/// `end` is a full frontier because the next gap check, or the lift, extends from it.
282#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
283pub struct Interval {
284	/// Root the block's consumption on this stream started from.
285	pub start: MmrRoot,
286	/// Frontier the block's consumption on this stream ended at.
287	pub end: MmrFrontier,
288}
289
290/// Written per block: the streams this block touched and the interval consumed on each, grouped by
291/// source. One interval per stream, keyed so the type enforces it; per-source order is the
292/// canonical `StreamId` order the lifts match against. The API view of a flat, host-append-only
293/// outbox vec. Encodes as the sorted vec of pairs would.
294#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug, TypeInfo, Default)]
295pub struct ConsumptionRecord {
296	/// `source -> stream -> interval`.
297	pub entries: BTreeMap<ParaId, BTreeMap<StreamId, Interval>>,
298}
299
300/// One source's streams with their intervals across a bundle, in block order. [`build_requires`]
301/// merges the per-block records into this and [`build_requires_entry`] lifts it.
302pub type SourceStreams = BTreeMap<StreamId, Vec<Interval>>;
303
304/// One lift, carried in the PoV. Matched by position to a source's `StreamId`-sorted record
305/// streams; a mispaired lift cannot verify because `tree_proof` binds the record's key.
306#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Eq, Debug, TypeInfo)]
307pub struct RequiresLift {
308	/// One extension per gap in the stream's interval chain, in gap order. Empty when there are no
309	/// gaps.
310	pub advances: Vec<MMRExtensionProof>,
311	/// Extends the chain's endpoint to the stream's current root. The identity extension when the
312	/// endpoint already is that root.
313	pub extension: MMRExtensionProof,
314	/// Keyed walk from the current stream root to the `StreamsRoot` the requires entry becomes.
315	pub tree_proof: TreeInclusionProof,
316}
317
318/// The candidate's lifts, grouped per source: the PoV transport form. `Decode` enforces strictly
319/// increasing `ParaId`s and bounds the source count at [`MAX_COMMITMENT_ENTRIES`], so more sources
320/// than a `RequiresSet` can hold are rejected before any lift is verified.
321/// [`TryFrom<BTreeMap>`](Self::try_from) is canonical by construction. [`build_requires`] zips
322/// these against the record's sources by position.
323#[derive(Clone, Encode, PartialEq, Eq, Debug, TypeInfo, Default)]
324pub struct LiftsBySource(BoundedVec<(ParaId, Vec<RequiresLift>), ConstU32<MAX_COMMITMENT_ENTRIES>>);
325
326impl LiftsBySource {
327	/// The sources, in strictly increasing order.
328	pub fn sources(&self) -> impl Iterator<Item = &ParaId> {
329		self.0.iter().map(|(source, _)| source)
330	}
331
332	/// `(source, lifts)` pairs, in source order.
333	pub fn iter(&self) -> impl Iterator<Item = &(ParaId, Vec<RequiresLift>)> {
334		self.0.iter()
335	}
336
337	pub fn len(&self) -> usize {
338		self.0.len()
339	}
340
341	pub fn is_empty(&self) -> bool {
342		self.0.is_empty()
343	}
344}
345
346impl TryFrom<BTreeMap<ParaId, Vec<RequiresLift>>> for LiftsBySource {
347	type Error = LiftError;
348
349	/// A map iterates in `ParaId` order with unique keys, so only the bound can fail.
350	fn try_from(map: BTreeMap<ParaId, Vec<RequiresLift>>) -> Result<Self, LiftError> {
351		BoundedVec::try_from(map.into_iter().collect::<Vec<_>>())
352			.map(Self)
353			.map_err(|_| LiftError::TooManySources)
354	}
355}
356
357/// Matches the derived `Encode`. The sequence must be strictly increasing by `ParaId`.
358impl Decode for LiftsBySource {
359	fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
360		let inner =
361			BoundedVec::<(ParaId, Vec<RequiresLift>), ConstU32<MAX_COMMITMENT_ENTRIES>>::decode(
362				input,
363			)?;
364		if inner.windows(2).any(|w| w[0].0 >= w[1].0) {
365			return Err("LiftsBySource: sources must be strictly increasing".into());
366		}
367		Ok(Self(inner))
368	}
369}
370
371impl DecodeWithMemTracking for LiftsBySource {}
372
373/// Why requires synthesis failed. All are deterministic in the records and PoV, so every validator
374/// reaches the same verdict.
375#[derive(Clone, Copy, PartialEq, Eq, Debug)]
376pub enum LiftError {
377	/// A stream's interval list was empty.
378	EmptyRecord,
379	/// A gap needed a forward-extension proof but `advances` was exhausted.
380	MissingAdvance,
381	/// `advances` carried more proofs than there were gaps.
382	StrayAdvance,
383	/// A gap's proof did not forward-extend the previous endpoint to the next start.
384	BrokenChain,
385	/// The endpoint extension did not verify against the stitched endpoint.
386	BadExtension,
387	/// The tree proof did not fold to a `StreamsRoot` (malformed / wrong key).
388	BadTreeProof,
389	/// A source's stream count and lift count disagreed.
390	LiftCountMismatch,
391	/// A source's streams lifted to more than one `StreamsRoot`.
392	DivergentRoots,
393	/// The recorded sources and the provided lifts' sources did not match exactly.
394	LiftSourceMismatch,
395	/// More sources than the `RequiresSet` bound allows.
396	TooManySources,
397}
398
399/// Stitch one stream's intervals, in bundle order, into its endpoint frontier. `advances` must hold
400/// exactly one extension per gap (a `start` that is not the previous `end`'s root), in gap order.
401/// Gaps are proven forward, so within a candidate verified states cannot regress; across candidates
402/// that is the STF's to hold.
403pub fn stitch(
404	intervals: &[Interval],
405	advances: &[MMRExtensionProof],
406) -> Result<MmrFrontier, LiftError> {
407	let (first, rest) = intervals.split_first().ok_or(LiftError::EmptyRecord)?;
408	let mut gaps = advances.iter();
409	// `first.start` is the boundary with the previous candidate; nothing here can check it, and
410	// nothing needs to: everything consumed is bound through the chain from `first.end`.
411	// Cross-candidate continuity is the STF's (the stored frontier for channels; for register
412	// reads, the last applied position and the register's monotonic fields).
413	let mut current = &first.end;
414	for next in rest {
415		let current_root = current.root();
416		if next.start != current_root {
417			// A gap must be a proven forward extension of where we ended.
418			let proof = gaps.next().ok_or(LiftError::MissingAdvance)?;
419			let extended = proof.calculate_root(current).map_err(|_| LiftError::BrokenChain)?;
420			if extended != next.start {
421				return Err(LiftError::BrokenChain);
422			}
423		}
424		current = &next.end;
425	}
426	if gaps.next().is_some() {
427		return Err(LiftError::StrayAdvance);
428	}
429	Ok(current.clone())
430}
431
432/// Lift one source's streams to a single committed `StreamsRoot`: stitch each stream's intervals
433/// into an endpoint, extend it to the stream's current root, walk the tree to the `StreamsRoot`,
434/// and require all of the source's streams to reach the same root. `streams` iterates in `StreamId`
435/// order; `lifts` matches it by position.
436pub fn build_requires_entry(
437	streams: &SourceStreams,
438	lifts: &[RequiresLift],
439) -> Result<StreamsRoot, LiftError> {
440	if streams.len() != lifts.len() {
441		return Err(LiftError::LiftCountMismatch);
442	}
443	let mut entry: Option<StreamsRoot> = None;
444	for ((stream, intervals), lift) in streams.iter().zip(lifts) {
445		let endpoint = stitch(intervals, &lift.advances)?;
446		// The endpoint is contained in the stream's current root, computed rather than declared,
447		let current =
448			lift.extension.calculate_root(&endpoint).map_err(|_| LiftError::BadExtension)?;
449		// and the keyed walk from it yields the `StreamsRoot`. The walk binds the record's `stream`
450		// key, so a mispaired lift cannot verify.
451		let root = streams_root_from_proof(*stream, current.0, &lift.tree_proof)
452			.ok_or(LiftError::BadTreeProof)?;
453		match entry {
454			None => entry = Some(root),
455			Some(prev) if prev != root => return Err(LiftError::DivergentRoots),
456			Some(_) => {},
457		}
458	}
459	entry.ok_or(LiftError::EmptyRecord)
460}
461
462/// Synthesize the candidate's [`RequiresSet`] from the per-block records, in bundle order, and the
463/// PoV lifts. Sources must match exactly; a recorded source without lifts, or lifts for an
464/// unrecorded source, invalidates the candidate.
465///
466/// `None` when the candidate consumed nothing: there is no entry the relay could match, and the
467/// candidate emits no `Requires` signal. `RequiresSet` cannot be empty.
468pub fn build_requires(
469	records: &[ConsumptionRecord],
470	lifts: &LiftsBySource,
471) -> Result<Option<RequiresSet>, LiftError> {
472	// Merge per (source, stream) intervals across the bundle, preserving block order.
473	let mut merged: BTreeMap<ParaId, SourceStreams> = BTreeMap::new();
474	for record in records {
475		for (source, streams) in &record.entries {
476			let by_stream = merged.entry(*source).or_default();
477			for (stream, interval) in streams {
478				by_stream.entry(*stream).or_default().push(interval.clone());
479			}
480		}
481	}
482	// Recorded sources and lift sources must be equal; both are ParaId-sorted.
483	if !merged.keys().eq(lifts.sources()) {
484		return Err(LiftError::LiftSourceMismatch);
485	}
486	if merged.is_empty() {
487		return Ok(None);
488	}
489	let entries = merged
490		.iter()
491		.zip(lifts.iter())
492		.map(|((source, by_stream), (_, source_lifts))| {
493			build_requires_entry(by_stream, source_lifts).map(|root| (*source, root))
494		})
495		.collect::<Result<Vec<_>, _>>()?;
496	RequiresSet::try_from_iter(entries)
497		.map(Some)
498		.map_err(|_| LiftError::TooManySources)
499}
500
501#[cfg(test)]
502mod tests {
503	use super::*;
504	use crate::{
505		streams_root::{gen_stream_proof, streams_root, TreeStep},
506		test_utils::{ch, extension, frontier_at, leaves, mmr_size},
507	};
508	use mmr_lib::{
509		ancestry_proof::ancestry_proof_positions,
510		util::{MemMMR, MemStore},
511	};
512	use polkadot_primitives::v9::MAX_COMMITMENT_ENTRIES;
513	use sp_core::H256;
514
515	/// Root over the first `k` leaves.
516	fn root_at(all: &[Hash], k: usize) -> MmrRoot {
517		frontier_at(all, k).root()
518	}
519
520	#[test]
521	fn extension_computes_root_and_rejects_tampering() {
522		let all = leaves(5);
523		let ext = extension(&all, 2, 5);
524		// Computes root@5 from frontier@2 + O(log n) connecting nodes (never a declared root).
525		assert_eq!(ext.calculate_root(&frontier_at(&all, 2)), Ok(root_at(&all, 5)));
526		// Wrong starting frontier → does not reconstruct the extended root.
527		assert_ne!(ext.calculate_root(&frontier_at(&all, 3)), Ok(root_at(&all, 5)));
528		// Tampered connecting node → a different computed root.
529		let mut bad = ext.clone();
530		bad.connecting_nodes[0] = H256::repeat_byte(0xff);
531		assert_ne!(bad.calculate_root(&frontier_at(&all, 2)), Ok(root_at(&all, 5)));
532		// Identity extension yields the frontier's own root.
533		let id = MMRExtensionProof::identity();
534		assert_eq!(id.calculate_root(&frontier_at(&all, 3)), Ok(root_at(&all, 3)));
535	}
536
537	/// `connecting_nodes` stores bare hashes, so the positions we derive must match what
538	/// `gen_ancestry_proof` emits. Checked for every `(old, new)` pair up to 256 leaves, which
539	/// includes the seven- and eight-peak sizes 127 and 255. A change in `mmr_lib` trips this;
540	/// `Cargo.lock` pins it.
541	#[test]
542	fn ancestry_positions_matches_mmr_lib() {
543		for n in 2..=256usize {
544			let all = leaves(n as u64);
545			let store = MemStore::<Hash>::default();
546			let mut mmr = MemMMR::<Hash, SpecMerge>::new(0, &store);
547			for l in &all {
548				mmr.push(*l).unwrap();
549			}
550			for k in 1..n {
551				let ap = mmr.gen_ancestry_proof(mmr_size(k)).unwrap();
552				let expected: Vec<u64> =
553					ap.prev_peaks_proof.proof_items().iter().map(|(p, _)| *p).collect();
554				let got = ancestry_proof_positions(mmr_size(k), mmr_size(n)).unwrap();
555				assert_eq!(got, expected, "position mismatch at k={k}, n={n}");
556			}
557		}
558	}
559
560	#[test]
561	fn extension_leaf_count_disambiguates_equal_node_counts() {
562		// 1→3 and 1→4 both take two nodes, placed differently, so only `leaf_count` tells them
563		// apart.
564		let all = leaves(4);
565		let to3 = extension(&all, 1, 3);
566		let to4 = extension(&all, 1, 4);
567		assert_eq!(to3.connecting_nodes.len(), 2);
568		assert_eq!(to4.connecting_nodes.len(), 2);
569		let from1 = frontier_at(&all, 1);
570		assert_eq!(to3.calculate_root(&from1), Ok(root_at(&all, 3)));
571		assert_eq!(to4.calculate_root(&from1), Ok(root_at(&all, 4)));
572		// The same two nodes under the other count place differently and reach neither root.
573		let relabelled =
574			MMRExtensionProof { leaf_count: 4, connecting_nodes: to3.connecting_nodes };
575		let got = relabelled.calculate_root(&from1);
576		assert_ne!(got, Ok(root_at(&all, 3)));
577		assert_ne!(got, Ok(root_at(&all, 4)));
578	}
579
580	#[test]
581	fn extension_verify_rejects_non_forward() {
582		let all = leaves(5);
583		// A real (non-identity) extension pointed *back* to the frontier's own size is not strictly
584		// forward → rejected up front, before any crypto work.
585		let mut ext = extension(&all, 2, 5);
586		ext.leaf_count = 2;
587		assert_eq!(ext.calculate_root(&frontier_at(&all, 2)), Err(ProofError::NotForward));
588	}
589
590	#[test]
591	fn extension_verify_bounds_untrusted_leaf_counts() {
592		let all = leaves(5);
593		// The proof's own count. A value whose derived node count would overflow `u64` is rejected
594		// before any position is computed from it.
595		let mut ext = extension(&all, 2, 5);
596		ext.leaf_count = u64::MAX;
597		assert_eq!(ext.calculate_root(&frontier_at(&all, 2)), Err(ProofError::LeafCountTooLarge));
598		ext.leaf_count = MAX_MMR_LEAF_COUNT + 1;
599		assert_eq!(ext.calculate_root(&frontier_at(&all, 2)), Err(ProofError::LeafCountTooLarge));
600		// The frontier's count is not this function's to check. An out-of-range `MmrFrontier`
601		// cannot be built, from `from_parts` or from `Decode`.
602		assert!(MmrFrontier::from_parts(vec![all[0]], 1 << 63).is_none());
603	}
604
605	#[test]
606	fn extension_verify_rejects_zero_leaf_count() {
607		let all = leaves(5);
608		// `0` with connecting nodes is not the identity: malformed, not merely non-forward.
609		let mut ext = extension(&all, 2, 5);
610		ext.leaf_count = 0;
611		assert_eq!(ext.calculate_root(&frontier_at(&all, 2)), Err(ProofError::InvalidProof));
612	}
613
614	#[test]
615	fn extension_identity_on_empty_frontier_yields_empty_root() {
616		// Yields `empty_root()`. An empty stream is never in the tree, so the tree walk is what
617		// fails.
618		assert_eq!(
619			MMRExtensionProof::identity().calculate_root(&MmrFrontier::default()),
620			Ok(MmrFrontier::default().root()),
621		);
622	}
623
624	#[test]
625	fn the_longest_valid_extension_fits_the_node_ceiling() {
626		// Worst case, one node per set bit: 1 → the largest all-ones count. The lift's PoV
627		// reservation relies on it.
628		let nodes =
629			ancestry_proof_positions(mmr_size(1), mmr_size((MAX_MMR_LEAF_COUNT - 1) as usize))
630				.unwrap()
631				.len();
632		assert_eq!(nodes, 48);
633		assert!(nodes <= MAX_MMR_HASHES as usize);
634	}
635
636	#[test]
637	fn over_long_extension_proof_fails_to_decode() {
638		// The bound is in the type, so an over-long proof is rejected at the length prefix.
639		let max = MAX_MMR_HASHES as usize;
640		let at_bound = (codec::Compact(5u64), vec![H256::zero(); max]).encode();
641		assert!(MMRExtensionProof::decode(&mut &at_bound[..]).is_ok());
642		let over = (codec::Compact(5u64), vec![H256::zero(); max + 1]).encode();
643		assert!(MMRExtensionProof::decode(&mut &over[..]).is_err());
644	}
645
646	#[test]
647	fn stitch_caught_up_needs_no_advance() {
648		let all = leaves(5);
649		let i1 = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) };
650		let i2 = Interval { start: root_at(&all, 2), end: frontier_at(&all, 5) };
651		assert_eq!(stitch(&[i1, i2], &[]).unwrap(), frontier_at(&all, 5));
652	}
653
654	#[test]
655	fn stitch_bridges_a_gap_and_flags_bad_advance_counts() {
656		let all = leaves(5);
657		let i1 = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) };
658		// Block 2 jumped forward to context @4 before continuing to @5.
659		let i2 = Interval { start: root_at(&all, 4), end: frontier_at(&all, 5) };
660		let advance = extension(&all, 2, 4);
661
662		assert_eq!(
663			stitch(&[i1.clone(), i2.clone()], &[advance.clone()]).unwrap(),
664			frontier_at(&all, 5)
665		);
666		// Missing the gap proof.
667		assert_eq!(stitch(&[i1.clone(), i2.clone()], &[]), Err(LiftError::MissingAdvance));
668		// A stray advance where the chain was already continuous.
669		let cont = Interval { start: root_at(&all, 2), end: frontier_at(&all, 5) };
670		assert_eq!(stitch(&[i1, cont], &[advance]), Err(LiftError::StrayAdvance));
671	}
672
673	#[test]
674	fn stitch_rejects_a_gap_bridged_to_the_wrong_context() {
675		// The advance is all that binds the earlier read to the stream: a context the chain does
676		// not reach must fail, or a fabricated read could hide behind a later genuine one.
677		let all = leaves(5);
678		let i1 = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) };
679		let i2 = Interval { start: root_at(&all, 4), end: frontier_at(&all, 5) };
680		// A genuine extension of @2, but to @3, not to the context @4 block 2 started from.
681		assert_eq!(
682			stitch(&[i1.clone(), i2.clone()], &[extension(&all, 2, 3)]),
683			Err(LiftError::BrokenChain)
684		);
685		// An extension that does not fit @2 at all (built from @1).
686		assert_eq!(stitch(&[i1, i2], &[extension(&all, 1, 4)]), Err(LiftError::BrokenChain));
687	}
688
689	#[test]
690	fn stitch_rejects_a_backward_step() {
691		// A later block read an older context. Extensions only go forward, so nothing can bridge
692		// it; the pallet does not record an ignored stale read for this reason.
693		let all = leaves(5);
694		let i1 = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 4) };
695		let i2 = Interval { start: root_at(&all, 2), end: frontier_at(&all, 2) };
696		assert_eq!(stitch(&[i1.clone(), i2.clone()], &[]), Err(LiftError::MissingAdvance));
697		assert_eq!(stitch(&[i1, i2], &[extension(&all, 4, 5)]), Err(LiftError::BrokenChain));
698	}
699
700	#[test]
701	fn nothing_recorded_lifts_nothing() {
702		assert_eq!(stitch(&[], &[]), Err(LiftError::EmptyRecord));
703		assert_eq!(build_requires_entry(&SourceStreams::new(), &[]), Err(LiftError::EmptyRecord));
704	}
705
706	#[test]
707	fn an_empty_source_entry_fails_closed() {
708		// The pallet records a source only with a stream. An entry with none has nothing to lift,
709		// so the candidate is invalid rather than silently requiring nothing from that source.
710		let source = ParaId::from(1000);
711		let record = ConsumptionRecord { entries: BTreeMap::from([(source, BTreeMap::new())]) };
712		let lifts = LiftsBySource::try_from(BTreeMap::from([(source, Vec::new())])).unwrap();
713		assert_eq!(build_requires(&[record], &lifts), Err(LiftError::EmptyRecord));
714	}
715
716	#[test]
717	fn consumption_record_encodes_as_the_sorted_vec_of_pairs() {
718		// The record crosses the runtime-API boundary; keying it must not move a byte relative to
719		// the design's `Vec<(StreamId, Interval)>` view of the same sorted, unique entries.
720		let all = leaves(3);
721		let (a, b) = (ch(1), ch(2));
722		let (ia, ib) = (
723			Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 1) },
724			Interval { start: root_at(&all, 1), end: frontier_at(&all, 3) },
725		);
726		let keyed = ConsumptionRecord {
727			entries: BTreeMap::from([(
728				ParaId::from(7),
729				BTreeMap::from([(b, ib.clone()), (a, ia.clone())]),
730			)]),
731		};
732		let as_vec: BTreeMap<ParaId, Vec<(StreamId, Interval)>> =
733			BTreeMap::from([(ParaId::from(7), vec![(a, ia), (b, ib)])]);
734		assert_eq!(keyed.encode(), as_vec.encode());
735	}
736
737	#[test]
738	fn hot_path_single_block_single_stream() {
739		let all = leaves(3);
740		let root3 = root_at(&all, 3);
741		let stream = ch(2000);
742		// StreamsRoot over the single active stream at root@3.
743		let entries = BTreeMap::from([(stream, root3.0)]);
744		let expected = streams_root(&entries).unwrap();
745		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
746
747		// One interval, endpoint = frontier@3; identity extension (caught up).
748		let interval = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) };
749		let lift = RequiresLift {
750			advances: Vec::new(),
751			extension: MMRExtensionProof::identity(),
752			tree_proof,
753		};
754		let streams = SourceStreams::from([(stream, vec![interval])]);
755		let root = build_requires_entry(&streams, &[lift]).unwrap();
756		assert_eq!(root, expected);
757	}
758
759	#[test]
760	fn build_requires_end_to_end_and_source_match() {
761		let all = leaves(3);
762		let root3 = root_at(&all, 3);
763		let source = ParaId::from(1000);
764		let stream = ch(2000);
765		let entries = BTreeMap::from([(stream, root3.0)]);
766		let expected = streams_root(&entries).unwrap();
767		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
768
769		let record = ConsumptionRecord {
770			entries: BTreeMap::from([(
771				source,
772				BTreeMap::from([(
773					stream,
774					Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) },
775				)]),
776			)]),
777		};
778		let mut lifts = BTreeMap::new();
779		lifts.insert(
780			source,
781			vec![RequiresLift {
782				advances: Vec::new(),
783				extension: MMRExtensionProof::identity(),
784				tree_proof,
785			}],
786		);
787
788		let lifts_by_source = LiftsBySource::try_from(lifts.clone()).unwrap();
789		let requires = build_requires(&[record.clone()], &lifts_by_source).unwrap().unwrap();
790		assert_eq!(requires.get(source), Some(&expected));
791
792		// A lift for an unrecorded source (or a missing one) is rejected.
793		let mut extra = lifts.clone();
794		extra.insert(ParaId::from(9999), Vec::new());
795		let extra = LiftsBySource::try_from(extra).unwrap();
796		assert_eq!(build_requires(&[record], &extra), Err(LiftError::LiftSourceMismatch));
797
798		// Nothing consumed, nothing lifted: no set, not an empty one.
799		assert_eq!(build_requires(&[], &LiftsBySource::default()), Ok(None));
800	}
801
802	#[test]
803	fn lifts_by_source_decode_enforces_canonical_form() {
804		let lift = || RequiresLift {
805			advances: Vec::new(),
806			extension: MMRExtensionProof::identity(),
807			tree_proof: StreamProof { steps: Default::default() },
808		};
809		let pair = |id: u32| (ParaId::from(id), vec![lift()]);
810		// Canonical round-trips …
811		let good = vec![pair(1), pair(2)].encode();
812		assert!(LiftsBySource::decode(&mut &good[..]).is_ok());
813		// … out of order and duplicate sources do not …
814		let unsorted = vec![pair(2), pair(1)].encode();
815		assert!(LiftsBySource::decode(&mut &unsorted[..]).is_err());
816		let duplicate = vec![pair(1), pair(1)].encode();
817		assert!(LiftsBySource::decode(&mut &duplicate[..]).is_err());
818		// … and more sources than a RequiresSet could hold are rejected before any verification.
819		let too_many: Vec<_> = (0..=MAX_COMMITMENT_ENTRIES).map(pair).collect();
820		assert!(LiftsBySource::decode(&mut &too_many.encode()[..]).is_err());
821		// A map is canonical by construction; only the bound can fail it.
822		let map: BTreeMap<_, _> = (0..=MAX_COMMITMENT_ENTRIES).map(pair).collect();
823		assert_eq!(LiftsBySource::try_from(map).err(), Some(LiftError::TooManySources));
824	}
825
826	#[test]
827	fn late_block_non_identity_extension_end_to_end() {
828		// The block ended at @2 and the committed root is at @5: a real extension bridges them.
829		let all = leaves(5);
830		let stream = ch(2000);
831		let committed = root_at(&all, 5);
832		let entries = BTreeMap::from([(stream, committed.0)]);
833		let expected = streams_root(&entries).unwrap();
834		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
835
836		let interval = Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) };
837		let lift =
838			RequiresLift { advances: Vec::new(), extension: extension(&all, 2, 5), tree_proof };
839		let streams = SourceStreams::from([(stream, vec![interval])]);
840		assert_eq!(build_requires_entry(&streams, &[lift]).unwrap(), expected);
841	}
842
843	#[test]
844	fn multi_stream_source_converges_on_one_root() {
845		// Two streams at different frontiers both lift to the source's one `StreamsRoot`.
846		let all = leaves(5);
847		let (a, b) = (ch(2000), ch(3000));
848		let (ra, rb) = (root_at(&all, 3), root_at(&all, 5));
849		let entries = BTreeMap::from([(a, ra.0), (b, rb.0)]);
850		let expected = streams_root(&entries).unwrap();
851		let (_pa, proof_a) = gen_stream_proof(&entries, a).unwrap();
852		let (_pb, proof_b) = gen_stream_proof(&entries, b).unwrap();
853
854		let lift = |tree_proof| RequiresLift {
855			advances: Vec::new(),
856			extension: MMRExtensionProof::identity(),
857			tree_proof,
858		};
859		// SourceStreams is keyed, so it iterates a, b — the order the lifts must match.
860		let streams = SourceStreams::from([
861			(a, vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }]),
862			(b, vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 5) }]),
863		]);
864		assert_eq!(
865			build_requires_entry(&streams, &[lift(proof_a), lift(proof_b)]).unwrap(),
866			expected
867		);
868	}
869
870	#[test]
871	fn build_requires_covers_multiple_sources() {
872		// Two sources, each one stream → a two-entry RequiresSet, each entry the source's own root.
873		let all = leaves(3);
874		let committed = root_at(&all, 3);
875		let (src_a, src_b) = (ParaId::from(1000), ParaId::from(2000));
876		let stream = ch(4000);
877		let entries = BTreeMap::from([(stream, committed.0)]);
878		let expected = streams_root(&entries).unwrap();
879
880		let record_for = |source| {
881			let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
882			let record = ConsumptionRecord {
883				entries: BTreeMap::from([(
884					source,
885					BTreeMap::from([(
886						stream,
887						Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) },
888					)]),
889				)]),
890			};
891			let lift = RequiresLift {
892				advances: Vec::new(),
893				extension: MMRExtensionProof::identity(),
894				tree_proof,
895			};
896			(record, lift)
897		};
898		let (ra, la) = record_for(src_a);
899		let (rb, lb) = record_for(src_b);
900		let lifts = LiftsBySource::try_from(BTreeMap::from([(src_a, vec![la]), (src_b, vec![lb])]))
901			.unwrap();
902
903		let requires = build_requires(&[ra, rb], &lifts).unwrap().unwrap();
904		assert_eq!(requires.len(), 2);
905		assert_eq!(requires.get(src_a), Some(&expected));
906		assert_eq!(requires.get(src_b), Some(&expected));
907	}
908
909	#[test]
910	fn build_requires_entry_stitches_a_bundle_gap() {
911		// A bundle whose second block jumped @2 → @4: an advance bridges the gap, then the
912		// extension reaches the committed @5.
913		let all = leaves(5);
914		let stream = ch(2000);
915		let committed = root_at(&all, 5);
916		let entries = BTreeMap::from([(stream, committed.0)]);
917		let expected = streams_root(&entries).unwrap();
918		let (_r, tree_proof) = gen_stream_proof(&entries, stream).unwrap();
919
920		let intervals = vec![
921			Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) },
922			Interval { start: root_at(&all, 4), end: frontier_at(&all, 5) }, // gap @2 → @4
923		];
924		let lift = RequiresLift {
925			advances: vec![extension(&all, 2, 4)],    // bridges the gap
926			extension: MMRExtensionProof::identity(), // endpoint @5 already the committed root
927			tree_proof,
928		};
929		let streams = SourceStreams::from([(stream, intervals)]);
930		assert_eq!(build_requires_entry(&streams, &[lift]).unwrap(), expected);
931	}
932
933	#[test]
934	fn lift_count_must_match_stream_count() {
935		let all = leaves(3);
936		let stream = ch(2000);
937		let streams = SourceStreams::from([(
938			stream,
939			vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }],
940		)]);
941		let lift = || RequiresLift {
942			advances: Vec::new(),
943			extension: MMRExtensionProof::identity(),
944			tree_proof: StreamProof { steps: Default::default() },
945		};
946		// One stream, no lifts, and one stream, two lifts — both mismatch.
947		assert_eq!(build_requires_entry(&streams, &[]), Err(LiftError::LiftCountMismatch));
948		assert_eq!(
949			build_requires_entry(&streams, &[lift(), lift()]),
950			Err(LiftError::LiftCountMismatch)
951		);
952	}
953
954	#[test]
955	fn build_requires_entry_rejects_a_backward_extension() {
956		// A forged extension that does not go strictly forward fails verification → BadExtension,
957		// before any root is synthesized.
958		let all = leaves(5);
959		let stream = ch(2000);
960		let (_r, tree_proof) =
961			gen_stream_proof(&BTreeMap::from([(stream, root_at(&all, 5).0)]), stream).unwrap();
962		let streams = SourceStreams::from([(
963			stream,
964			vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }],
965		)]);
966		// leaf_count 1 <= the endpoint's 3 leaves → NotForward inside `calculate_root` →
967		// BadExtension.
968		let lift = RequiresLift {
969			advances: Vec::new(),
970			extension: MMRExtensionProof { leaf_count: 1, connecting_nodes: BoundedVec::new() },
971			tree_proof,
972		};
973		assert_eq!(build_requires_entry(&streams, &[lift]), Err(LiftError::BadExtension));
974	}
975
976	#[test]
977	fn stitch_ignores_the_first_interval_start() {
978		// `first.start` is the boundary with the previous candidate and is deliberately unchecked;
979		// garbage there does not affect the stitched endpoint.
980		let all = leaves(5);
981		let garbage =
982			Interval { start: MmrRoot(H256::repeat_byte(0xEE)), end: frontier_at(&all, 2) };
983		let cont = Interval { start: root_at(&all, 2), end: frontier_at(&all, 5) };
984		assert_eq!(stitch(&[garbage, cont], &[]).unwrap(), frontier_at(&all, 5));
985	}
986
987	#[test]
988	fn build_requires_entry_rejects_a_malformed_tree_proof() {
989		// A hand-built step with an out-of-range split bit (a one-stream tree has no real steps).
990		let all = leaves(3);
991		let stream = ch(2000);
992		let streams = SourceStreams::from([(
993			stream,
994			vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }],
995		)]);
996		// `split_bit == KEY_BITS` (64) is past the key; the fold rejects it.
997		let bad = StreamProof {
998			steps: BoundedVec::try_from(vec![TreeStep { split_bit: 64, sibling: H256::zero() }])
999				.unwrap(),
1000		};
1001		let lift = RequiresLift {
1002			advances: Vec::new(),
1003			extension: MMRExtensionProof::identity(),
1004			tree_proof: bad,
1005		};
1006		assert_eq!(build_requires_entry(&streams, &[lift]), Err(LiftError::BadTreeProof));
1007	}
1008
1009	#[test]
1010	fn build_requires_entry_flags_missing_and_stray_advances() {
1011		// stitch's advance-count errors, reached through the entry path rather than in isolation.
1012		let all = leaves(5);
1013		let stream = ch(2000);
1014		let (_r, tree_proof) =
1015			gen_stream_proof(&BTreeMap::from([(stream, root_at(&all, 5).0)]), stream).unwrap();
1016		let mk = |intervals, advances| {
1017			let lift = RequiresLift {
1018				advances,
1019				extension: MMRExtensionProof::identity(),
1020				tree_proof: tree_proof.clone(),
1021			};
1022			build_requires_entry(&SourceStreams::from([(stream, intervals)]), &[lift])
1023		};
1024
1025		// Gap @2 → @4 with no advance to bridge it.
1026		let gapped = vec![
1027			Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) },
1028			Interval { start: root_at(&all, 4), end: frontier_at(&all, 5) },
1029		];
1030		assert_eq!(mk(gapped, Vec::new()), Err(LiftError::MissingAdvance));
1031
1032		// Continuous @2 → @2 but an advance supplied anyway.
1033		let continuous = vec![
1034			Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 2) },
1035			Interval { start: root_at(&all, 2), end: frontier_at(&all, 5) },
1036		];
1037		assert_eq!(mk(continuous, vec![extension(&all, 2, 4)]), Err(LiftError::StrayAdvance));
1038	}
1039
1040	#[test]
1041	fn divergent_roots_rejected() {
1042		let all = leaves(3);
1043		let root3 = root_at(&all, 3);
1044		let (sa, sb) = (ch(2000), ch(3000));
1045
1046		// Two single-stream trees → each stream's tree_proof folds to a DIFFERENT StreamsRoot.
1047		let (_ra, proof_a) = gen_stream_proof(&BTreeMap::from([(sa, root3.0)]), sa).unwrap();
1048		let (_rb, proof_b) = gen_stream_proof(&BTreeMap::from([(sb, root3.0)]), sb).unwrap();
1049
1050		let mk = |tree_proof| RequiresLift {
1051			advances: Vec::new(),
1052			extension: MMRExtensionProof::identity(),
1053			tree_proof,
1054		};
1055		let streams = SourceStreams::from([
1056			(sa, vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }]),
1057			(sb, vec![Interval { start: MmrRoot(H256::zero()), end: frontier_at(&all, 3) }]),
1058		]);
1059		assert_eq!(
1060			build_requires_entry(&streams, &[mk(proof_a), mk(proof_b)]),
1061			Err(LiftError::DivergentRoots),
1062		);
1063	}
1064
1065	/// Single-leaf inclusion proof for leaf `index` of an `n`-leaf MMR (`mmr_lib`'s `gen_proof`).
1066	fn inclusion(all: &[Hash], n: usize, index: usize) -> MmrInclusionProof {
1067		let store = MemStore::<Hash>::default();
1068		let mut mmr = MemMMR::<Hash, SpecMerge>::new(0, &store);
1069		for l in &all[..n] {
1070			mmr.push(*l).unwrap();
1071		}
1072		let proof = mmr.gen_proof(vec![leaf_index_to_pos(index as u64)]).unwrap();
1073		MmrInclusionProof {
1074			mmr_size: mmr_size(n),
1075			items: BoundedVec::truncate_from(proof.proof_items().to_vec()),
1076		}
1077	}
1078
1079	#[test]
1080	fn inclusion_verify_head_pins_head_and_derives_root() {
1081		let all = leaves(6);
1082		// 6 leaves → head = index 5 (a lone smallest peak with a 1-step sibling path AND one other
1083		// peak: count_ones(6) - 1 = 1), exercising both halves of the item split.
1084		let proof = inclusion(&all, 6, 5);
1085
1086		// Correct head leaf → head position 5 + the frontier of exactly these 6 leaves.
1087		let (pos, frontier) = proof.verify_head(all[5]).expect("well-formed head proof");
1088		assert_eq!(pos, MessagePosition(5));
1089		assert_eq!(frontier.leaf_count(), 6);
1090		assert_eq!(frontier.root(), root_at(&all, 6));
1091
1092		// Another leaf under the same proof gives a different root, so an old leaf can't pass as
1093		// the head.
1094		let (_, forged) = proof.verify_head(all[0]).expect("shape still parses");
1095		assert_ne!(forged.root(), root_at(&all, 6));
1096
1097		// Wrong item count (one extra item) → structural reject.
1098		let mut bad = proof.clone();
1099		bad.items.try_push(H256::zero()).expect("below the bound; qed");
1100		assert_eq!(bad.verify_head(all[5]), Err(ProofError::InvalidProof));
1101
1102		// Invalid `mmr_size` (2 is not a valid MMR node count) → reject, no panic.
1103		assert_eq!(
1104			MmrInclusionProof { mmr_size: 2, items: BoundedVec::new() }.verify_head(all[5]),
1105			Err(ProofError::InvalidProof),
1106		);
1107	}
1108
1109	#[test]
1110	fn inclusion_verify_head_boundary_shapes() {
1111		// The `path_len = trailing_zeros` / `other_peaks = count_ones - 1` formulas at their
1112		// extremes.
1113
1114		// n = 1: a single-leaf MMR. The head proof has zero items (no sibling path, no other
1115		// peak).
1116		let all1 = leaves(1);
1117		let (pos1, f1) = inclusion(&all1, 1, 0).verify_head(all1[0]).expect("n=1 head");
1118		assert_eq!(pos1, MessagePosition(0));
1119		assert_eq!(f1.leaf_count(), 1);
1120		assert_eq!(f1.root(), root_at(&all1, 1));
1121
1122		// n = 8: a perfect single-peak MMR. A full 3-step sibling path with no other peaks.
1123		let all8 = leaves(8);
1124		let (pos8, f8) = inclusion(&all8, 8, 7).verify_head(all8[7]).expect("n=8 head");
1125		assert_eq!(pos8, MessagePosition(7));
1126		assert_eq!(f8.leaf_count(), 8);
1127		assert_eq!(f8.root(), root_at(&all8, 8));
1128	}
1129
1130	#[test]
1131	fn inclusion_verify_leaf_binds_position_and_value() {
1132		let all = leaves(6);
1133		let want = root_at(&all, 6);
1134		let proof = inclusion(&all, 6, 3);
1135
1136		// Correct leaf at its position → the committed stream root.
1137		assert_eq!(proof.verify_leaf(MessagePosition(3), all[3]), Ok(want));
1138		// Tampered leaf value → a different root (never the committed one).
1139		assert_ne!(proof.verify_leaf(MessagePosition(3), H256::repeat_byte(0xAA)), Ok(want));
1140		// Correct leaf claimed at the wrong position → a different root.
1141		assert_ne!(proof.verify_leaf(MessagePosition(4), all[3]), Ok(want));
1142		// Position at/after the leaf count (6 leaves → valid 0..=5) → reject.
1143		assert_eq!(
1144			proof.verify_leaf(MessagePosition(6), all[3]),
1145			Err(ProofError::PositionOutOfRange)
1146		);
1147		// Invalid `mmr_size` → reject, no panic.
1148		assert_eq!(
1149			MmrInclusionProof { mmr_size: 2, items: BoundedVec::new() }
1150				.verify_leaf(MessagePosition(0), all[0]),
1151			Err(ProofError::InvalidProof),
1152		);
1153	}
1154
1155	#[test]
1156	fn oversized_inclusion_proof_fails_to_decode() {
1157		// The bound is in the type, so an oversized proof is rejected at the length prefix.
1158		let max = MAX_INCLUSION_PROOF_ITEMS as usize;
1159		let at_bound = (mmr_size(6) as u64, vec![H256::zero(); max]).encode();
1160		assert!(MmrInclusionProof::decode(&mut &at_bound[..]).is_ok());
1161		let over = (mmr_size(6) as u64, vec![H256::zero(); max + 1]).encode();
1162		assert!(MmrInclusionProof::decode(&mut &over[..]).is_err());
1163	}
1164
1165	/// Found by the `lift_inclusion` fuzzer: a padded 1-leaf proof used to verify.
1166	#[test]
1167	fn inclusion_verify_leaf_rejects_padding() {
1168		let all = leaves(6);
1169		let one = inclusion(&all, 1, 0);
1170		assert!(one.items.is_empty());
1171		assert_eq!(one.verify_leaf(MessagePosition(0), all[0]), Ok(root_at(&all, 1)));
1172		let mut padded = one.clone();
1173		padded.items.try_push(H256::repeat_byte(0xAB)).expect("below the bound; qed");
1174		assert_eq!(padded.verify_leaf(MessagePosition(0), all[0]), Err(ProofError::InvalidProof));
1175
1176		let mut longer = inclusion(&all, 6, 3);
1177		longer.items.try_push(H256::zero()).expect("below the bound; qed");
1178		assert_eq!(longer.verify_leaf(MessagePosition(3), all[3]), Err(ProofError::InvalidProof));
1179	}
1180
1181	/// 130 leaves gets us past 127, the first count with seven peaks.
1182	#[test]
1183	fn leaf_proof_len_matches_mmr_lib() {
1184		let all = leaves(130);
1185		for n in 1..=130 {
1186			for i in 0..n {
1187				assert_eq!(
1188					leaf_proof_len(n as u64, i as u64),
1189					inclusion(&all, n, i).items.len(),
1190					"leaf {i} of {n}"
1191				);
1192			}
1193		}
1194	}
1195
1196	#[test]
1197	fn inclusion_verify_head_rejects_non_head_proof() {
1198		let all = leaves(6);
1199		// Leaf 2's proof has 3 items; a 6-leaf head proof has 2.
1200		let non_head = inclusion(&all, 6, 2);
1201		assert_eq!(non_head.verify_head(all[2]), Err(ProofError::InvalidProof));
1202	}
1203
1204	#[test]
1205	fn inclusion_head_and_leaf_agree_on_root() {
1206		// `verify_head`'s own peak walk and `mmr_lib::calculate_root` must agree.
1207		let all = leaves(6);
1208		let proof = inclusion(&all, 6, 5);
1209		let (pos, frontier) = proof.verify_head(all[5]).expect("head verifies");
1210		assert_eq!(pos, MessagePosition(5));
1211		let via_head = frontier.root();
1212		let via_leaf = proof.verify_leaf(pos, all[5]).expect("leaf verifies");
1213		assert_eq!(via_head, via_leaf);
1214		assert_eq!(via_head, root_at(&all, 6));
1215	}
1216}