Skip to main content

mkit_core/
sparse.rs

1//! Verifiable sparse-checkout (Phase 1 scaffold).
2//!
3//! Spec reference: `docs/SPEC-SPARSE-CHECKOUT.md`. Issue #158.
4//!
5//! # What this is
6//!
7//! Today's `mkit sparse-checkout` filters paths *on the client* after
8//! the server has handed over the full tree. That's fine for the file
9//! transport but wasteful on HTTP / S3 transports where the server
10//! could ship a partial subtree if the client could *verify* the
11//! server didn't lie about which entries were omitted by request
12//! versus silently dropped.
13//!
14//! This module is the Phase 1 core scaffolding: build a manifest from
15//! a `Tree` + filter, and verify a delivered set of `TreeEntry`s
16//! against it. The actual transport-level integration (HTTP/S3 query
17//! params, on-disk bitmap cache) is Phase 2 and is intentionally out
18//! of scope.
19//!
20//! # Authenticated bitmap
21//!
22//! Authentication uses
23//! [`commonware_storage::AuthenticatedBitMap`][bitmap], which provides
24//! a Merkleized bitmap with bit-level inclusion proofs. The bitmap is
25//! `ALPHA`-tier upstream and `std`-only, so this entire module sits
26//! behind the `sparse-checkout` Cargo feature (default off).
27//!
28//! Each entry in the underlying `Tree` is assigned a leaf index equal
29//! to its position in the tree's strict lexicographic byte ordering
30//! (the same ordering enforced by [`Tree::is_sorted`]). A bit set at
31//! index `i` means "the server is shipping entry `i`"; an unset bit
32//! means "this entry is omitted by client request". Tampering — the
33//! server flipping a bit or omitting/inserting an entry — produces a
34//! different bitmap root, which fails verification against the
35//! root committed in the [`SparseManifest`].
36//!
37//! # Wire format
38//!
39//! Strictly defined by `docs/SPEC-SPARSE-CHECKOUT.md`. Phase 1 does
40//! not yet wire `SparseProof` into any transport — the type is the
41//! in-memory carrier between [`build_sparse`] and [`verify_sparse`].
42//!
43//! [bitmap]: https://docs.rs/commonware-storage
44
45use crate::hash::{Hash, Hasher, ZERO, hash as blake3_hash};
46use crate::object::{EntryMode, Object, Tree, TreeEntry};
47use crate::serialize::serialize;
48use std::path::PathBuf;
49
50use commonware_cryptography::{Sha256, sha256};
51use commonware_runtime::{Metrics as _, Runner as _, deterministic};
52use commonware_storage::{MerkleizedBitMap, merkle::mmr};
53
54/// Bitmap chunk size in bytes (32 bytes = 256 bits = one SHA-256 digest).
55///
56/// Chosen to match the upstream hasher digest size, which is the
57/// upstream recommendation for minimising proof size.
58const CHUNK_BYTES: usize = 32;
59
60/// Hard cap on the number of leaves in a tree we are willing to build
61/// a sparse manifest for. Matches the per-tree `entry_count` bound in
62/// SPEC-OBJECTS §4. Verifier MUST enforce the same cap so a malicious
63/// `manifest.leaf_count` can't allocate unbounded memory.
64pub const MAX_LEAVES: u64 = 1_000_000;
65
66/// Hard cap on the number of filter paths. Prevents a hostile client
67/// from sending a billion-entry filter to a server. Mirrors the
68/// transport-side bound documented in SPEC-SPARSE-CHECKOUT §4.
69pub const MAX_FILTER_PATHS: usize = 100_000;
70
71/// Manifest committing to which tree entries the server is including
72/// in a sparse delivery. See SPEC-SPARSE-CHECKOUT §2.
73///
74/// All three fields are 32-byte BLAKE3 / SHA-256 digests. They are
75/// length-prefixed and content-addressed independently so a
76/// downstream codec can serialise them in any order.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub struct SparseManifest {
79    /// BLAKE3 hash of the full tree object the manifest is derived
80    /// from. Lets the client correlate this manifest with the
81    /// `tree_hash` it asked the server for.
82    pub tree_hash: Hash,
83    /// Root of the [`MerkleizedBitMap`] over the include / exclude
84    /// bitmap. SHA-256 (32 bytes) under the upstream `mmr::Family`
85    /// hasher. Verifier MUST recompute this from the delivered
86    /// bitmap chunks and reject on mismatch.
87    pub bitmap_root: Hash,
88    /// BLAKE3 hash of the canonicalised filter — see
89    /// [`hash_filter`]. Binds the manifest to a specific filter so
90    /// the server can't substitute a different one mid-transfer.
91    pub filter_hash: Hash,
92    /// Total number of leaves in the source tree (= bitmap length in
93    /// bits). Bounded by [`MAX_LEAVES`].
94    pub leaf_count: u64,
95}
96
97/// Verifiable proof bundle accompanying a [`SparseManifest`].
98///
99/// Phase 1 carries the full bitmap chunks; for any realistic tree
100/// size the bitmap fits comfortably in a few hundred bytes and the
101/// verifier walks every delivered entry anyway. Phase 2's transport
102/// wire-format may add per-bit inclusion proofs if bandwidth ever
103/// becomes a concern — those will land as a new field, not as a swap.
104#[derive(Debug, Clone)]
105pub struct SparseProof {
106    /// The raw bitmap bytes, exactly `ceil(leaf_count / 8)` bytes
107    /// padded to a chunk boundary (multiple of `CHUNK_BYTES`).
108    /// Verifier MUST recompute the bitmap root from these bytes and
109    /// compare to `manifest.bitmap_root`.
110    pub bitmap_bytes: Vec<u8>,
111}
112
113/// Stable BLAKE3 hash of a path-prefix filter. Canonical form:
114///
115/// 1. Sort the filter lexicographically by raw bytes.
116/// 2. Deduplicate.
117/// 3. For each path, append `len: u32 LE` then UTF-8 bytes.
118/// 4. Hash the resulting buffer with BLAKE3.
119///
120/// The empty filter hashes to `BLAKE3([])`, not `ZERO`. An empty
121/// filter is a valid manifest committing to "no entries delivered".
122#[must_use]
123pub fn hash_filter(filter: &[PathBuf]) -> Hash {
124    let mut canonical: Vec<&[u8]> = filter
125        .iter()
126        .filter_map(|p| p.to_str().map(str::as_bytes))
127        .collect();
128    canonical.sort_unstable();
129    canonical.dedup();
130
131    let mut h = Hasher::new();
132    for bytes in &canonical {
133        let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
134        h.update(&len.to_le_bytes());
135        h.update(bytes);
136    }
137    h.finalize()
138}
139
140/// Returns `true` if `entry.name` is selected by *any* prefix in
141/// `filter`. The filter is interpreted as a list of path-prefixes;
142/// an empty filter selects nothing (it commits to "no entries").
143///
144/// Semantics:
145///
146/// * A filter path exactly equal to the entry name matches.
147/// * A filter path that is a strict prefix of the entry name matches
148///   only when followed by a `/` byte; this prevents `foo` from
149///   matching `foobar`.
150fn entry_matches_filter(entry: &TreeEntry, filter: &[PathBuf]) -> bool {
151    let name = entry.name.as_slice();
152    for path in filter {
153        let Some(bytes) = path.to_str().map(str::as_bytes) else {
154            continue;
155        };
156        if bytes.is_empty() {
157            continue;
158        }
159        if name == bytes {
160            return true;
161        }
162        if name.len() > bytes.len() && name.starts_with(bytes) && name[bytes.len()] == b'/' {
163            return true;
164        }
165    }
166    false
167}
168
169/// Errors raised by [`build_sparse`] and [`verify_sparse`]. Phase 1
170/// keeps this small — the transport layer will wrap these in its own
171/// error type in Phase 2.
172#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
173pub enum SparseError {
174    /// Source tree has more entries than [`MAX_LEAVES`]. The bitmap
175    /// would still build, but we refuse out of caution: an attacker
176    /// shouldn't be able to force a multi-GB allocation on the
177    /// verifier by claiming a huge `leaf_count`.
178    #[error("tree has {actual} entries, exceeds MAX_LEAVES = {}", MAX_LEAVES)]
179    TooManyLeaves { actual: u64 },
180    /// Filter has more entries than [`MAX_FILTER_PATHS`].
181    #[error(
182        "filter has {actual} paths, exceeds MAX_FILTER_PATHS = {}",
183        MAX_FILTER_PATHS
184    )]
185    TooManyFilterPaths { actual: usize },
186    /// Source tree's entries are not in strict lex order. Our leaf
187    /// indices are defined by that order, so we refuse to build a
188    /// manifest from a tree that violates the invariant.
189    #[error("source tree entries are not lex-sorted; refusing to build manifest")]
190    UnsortedTree,
191}
192
193/// Build a sparse manifest from a tree and a filter.
194///
195/// Walks `tree.entries` in canonical order (which, per
196/// SPEC-OBJECTS §4, is byte-wise lex order on `name`). For each
197/// entry, sets bit `i` in the underlying [`MerkleizedBitMap`] iff
198/// any prefix in `filter` selects that entry's name.
199///
200/// Returns
201///
202/// * the subset of `tree.entries` selected by the filter (the ones
203///   the server would actually ship under a server-side sparse
204///   delivery), in the same canonical order;
205/// * the [`SparseManifest`] committing to that subset;
206/// * the [`SparseProof`] the verifier needs to check the manifest.
207///
208/// # Errors
209///
210/// * [`SparseError::UnsortedTree`] — `tree.entries` violates the
211///   spec-mandated lex ordering.
212/// * [`SparseError::TooManyLeaves`] — `tree.entries.len() > MAX_LEAVES`.
213/// * [`SparseError::TooManyFilterPaths`] — `filter.len() > MAX_FILTER_PATHS`.
214///
215/// # Panics
216///
217/// Never panics on caller input. May abort the in-process commonware
218/// async runtime on an upstream bug; we treat that as a programmer
219/// error because the bitmap is in-memory only and has no real I/O
220/// paths to fail on.
221pub fn build_sparse(
222    tree: &Tree,
223    filter: &[PathBuf],
224) -> Result<(Vec<TreeEntry>, SparseManifest, SparseProof), SparseError> {
225    let leaf_count = u64::try_from(tree.entries.len()).unwrap_or(u64::MAX);
226    if leaf_count > MAX_LEAVES {
227        return Err(SparseError::TooManyLeaves { actual: leaf_count });
228    }
229    if filter.len() > MAX_FILTER_PATHS {
230        return Err(SparseError::TooManyFilterPaths {
231            actual: filter.len(),
232        });
233    }
234    if !tree.is_sorted() {
235        return Err(SparseError::UnsortedTree);
236    }
237
238    // Compute the include-bit vector and pull out the entries we'd
239    // actually ship. The bitmap is just a Vec<bool> at this point;
240    // we hand it to the merkleized bitmap below.
241    let mut bits: Vec<bool> = Vec::with_capacity(tree.entries.len());
242    let mut delivered: Vec<TreeEntry> = Vec::new();
243    for entry in &tree.entries {
244        let include = entry_matches_filter(entry, filter);
245        bits.push(include);
246        if include {
247            delivered.push(entry.clone());
248        }
249    }
250
251    // Build the bitmap inside the upstream's async runtime. The
252    // deterministic runner is an in-memory test executor — no real
253    // I/O, no network — which is exactly what we want here: the
254    // bitmap is purely a verifiable commitment object, not a
255    // persistent store.
256    let (bitmap_root, bitmap_bytes) = merkleize_bits(&bits);
257
258    let manifest = SparseManifest {
259        tree_hash: tree_hash(tree),
260        bitmap_root,
261        filter_hash: hash_filter(filter),
262        leaf_count,
263    };
264    let proof = SparseProof { bitmap_bytes };
265    Ok((delivered, manifest, proof))
266}
267
268/// Build the upstream `MerkleizedBitMap` over `bits` and return
269/// `(root, bitmap_bytes)`. Shared between [`build_sparse`] and
270/// [`verify_sparse`] so the two cannot drift.
271///
272/// Phase 1 spins a fresh `deterministic::Runner` per call. Phase 2
273/// (sparse over a real transport) will reuse a long-lived executor via
274/// the future `mkit_core::protocol::Executor` shim; the dependency is
275/// captured at this seam to keep the migration mechanical.
276fn merkleize_bits(bits: &[bool]) -> (Hash, Vec<u8>) {
277    let runner = deterministic::Runner::default();
278    let bits_owned = bits.to_vec();
279    runner.start(move |ctx| async move {
280        let hasher = mmr::StandardHasher::<Sha256>::new();
281        let bitmap: MerkleizedBitMap<_, sha256::Digest, CHUNK_BYTES> =
282            MerkleizedBitMap::init(ctx.with_label("sparse"), "sparse", None, &hasher)
283                .await
284                .expect("in-memory bitmap init cannot fail");
285        let mut dirty = bitmap.into_dirty();
286        for b in &bits_owned {
287            dirty.push(*b);
288        }
289        let merkleized = dirty.merkleize(&hasher).expect("merkleize is infallible");
290        let root = merkleized.root();
291
292        let mut bytes = Vec::with_capacity(bits_owned.len().div_ceil(8));
293        for (i, bit) in bits_owned.iter().enumerate() {
294            if i % 8 == 0 {
295                bytes.push(0u8);
296            }
297            if *bit {
298                let last = bytes.last_mut().expect("just pushed a byte");
299                *last |= 1 << (i % 8);
300            }
301        }
302
303        let mut root_bytes: Hash = [0u8; 32];
304        root_bytes.copy_from_slice(root.as_ref());
305        (root_bytes, bytes)
306    })
307}
308
309/// Verify a sparse delivery against a manifest.
310///
311/// Returns `true` iff *all* of the following hold:
312///
313/// 1. `manifest.leaf_count <= MAX_LEAVES`.
314/// 2. `manifest.filter_hash == hash_filter(filter)` — the manifest
315///    was issued against the same filter the client supplied.
316/// 3. The set of leaf-indices implied by `bitmap_bytes` matches
317///    the canonical leaf-indices the filter would select.
318/// 4. The bitmap reconstructed from `bitmap_bytes` hashes to
319///    `manifest.bitmap_root` under the upstream bitmap commitment.
320/// 5. `delivered_entries`, in order, are *exactly* the entries
321///    whose leaf-index has its bit set.
322///
323/// Phase 1 cannot independently check `tree_hash` because the
324/// verifier doesn't have the full tree (that's the whole point of
325/// sparse delivery). The Phase 2 transport layer will recompute the
326/// tree hash once it has assembled enough of the structure.
327///
328/// # Panics
329///
330/// Never. All failure modes return `false`.
331#[must_use]
332pub fn verify_sparse(
333    manifest: &SparseManifest,
334    delivered_entries: &[TreeEntry],
335    filter: &[PathBuf],
336    proof: &SparseProof,
337) -> bool {
338    // (1) Sanity caps. A hostile manifest could claim 2^63 leaves;
339    // refuse before we allocate anything proportional to it.
340    if manifest.leaf_count > MAX_LEAVES {
341        return false;
342    }
343    if filter.len() > MAX_FILTER_PATHS {
344        return false;
345    }
346
347    // (2) Filter binding. Cheap and catches the "server swapped the
348    // filter" attack early.
349    if manifest.filter_hash != hash_filter(filter) {
350        return false;
351    }
352
353    // The bitmap must be exactly enough bytes to hold `leaf_count`
354    // bits, with no extra trailing bytes (otherwise an attacker
355    // could pad with extra "set" bits the manifest never committed
356    // to). `usize` is safe because we just bounded leaf_count by
357    // MAX_LEAVES = 1M; refuse on impossibly large 32-bit casts.
358    let Ok(leaf_count) = usize::try_from(manifest.leaf_count) else {
359        return false;
360    };
361    let expected_bitmap_bytes = leaf_count.div_ceil(8);
362    if proof.bitmap_bytes.len() != expected_bitmap_bytes {
363        return false;
364    }
365
366    // (3) + (5) Walk bits and delivered entries together.
367    //
368    // We can't fully verify (5) without seeing the source tree, but
369    // we can verify the *count* of set bits matches the count of
370    // delivered entries, and that delivered entries are themselves
371    // selected by the filter. The Phase 2 transport will cross-check
372    // delivered_entries[i] against tree position once it has the
373    // canonical leaf-index → name mapping.
374    let mut set_bits = 0usize;
375    for i in 0..leaf_count {
376        let byte = proof.bitmap_bytes[i / 8];
377        if (byte >> (i % 8)) & 1 == 1 {
378            set_bits += 1;
379        }
380    }
381    if set_bits != delivered_entries.len() {
382        return false;
383    }
384    for entry in delivered_entries {
385        if !entry_matches_filter(entry, filter) {
386            return false;
387        }
388    }
389
390    // (4) Reconstruct the bitmap root and compare. Rebuilds the
391    // bitmap inside an in-memory commonware runtime, identical to
392    // the one [`build_sparse`] used. Any tampering with
393    // `bitmap_bytes` produces a different root.
394    let bits: Vec<bool> = (0..leaf_count)
395        .map(|i| (proof.bitmap_bytes[i / 8] >> (i % 8)) & 1 == 1)
396        .collect();
397    let (computed_root, _bytes) = merkleize_bits(&bits);
398    if computed_root != manifest.bitmap_root {
399        return false;
400    }
401
402    true
403}
404
405/// Compute the canonical SPEC-OBJECTS tree hash.
406///
407/// Phase 2 swaps the Phase-1 placeholder for the canonical hash:
408/// `BLAKE3(serialize(Object::Tree(t)))`. This is the *same* digest the
409/// rest of the codebase uses to address a tree object — commits, remix
410/// roots, and the object store all key trees by this value.
411///
412/// Binding the manifest to the canonical tree hash means the verifier
413/// can cross-check the manifest against any independently-known
414/// commitment to the source tree (a parent commit's `tree_hash`, a
415/// merge base's tree, the local object store) without re-deriving a
416/// sparse-private digest.
417///
418/// The empty tree still serializes to a valid 10-byte object
419/// (`6-byte prologue || u32 LE 0` entries) and hashes to a non-zero
420/// digest — there is no longer a `ZERO` short-circuit.
421#[must_use]
422pub fn tree_hash(tree: &Tree) -> Hash {
423    // `serialize` only fails when an individual length-prefixed field
424    // overflows `u32` — `Tree::entries` is bounded above by
425    // `MAX_LEAVES` (1 M) here so the serialise call cannot fail on any
426    // tree that already passed our caller guards. We still return
427    // `ZERO` as a defensive fallback to keep the function infallible
428    // for downstream users — they have no recourse if the upstream
429    // tree is malformed.
430    let Ok(bytes) = serialize(&Object::Tree(tree.clone())) else {
431        return ZERO;
432    };
433    blake3_hash(&bytes)
434}
435
436/// Wire envelope magic — `b"MSP1"` (mkit-sparse-v1). Helps the
437/// transport sanity-check the body before any deserialisation. Sits in
438/// the same family as the v1 object prologue, but the codes are
439/// distinct so a misrouted blob can't be parsed as a sparse response.
440pub const SPARSE_WIRE_MAGIC: [u8; 4] = *b"MSP1";
441
442/// Wire-format version of [`SparseResponse`]. Bumped on any
443/// non-backward-compatible change.
444pub const SPARSE_WIRE_VERSION: u8 = 0x01;
445
446/// Maximum encoded sparse-response wire size. Caller-side cap; ~16 MiB
447/// is comfortably more than a maximum-sized bitmap (~125 KB) plus the
448/// largest possible entry stream.
449pub const SPARSE_WIRE_MAX_BYTES: usize = 16 * 1024 * 1024;
450
451/// Complete server-to-client sparse delivery: manifest + entries +
452/// proof, in the order they appear on the wire. The encoder and
453/// decoder are content-stable across calls so the byte layout can be
454/// pinned in golden vectors if and when needed.
455#[derive(Debug, Clone)]
456pub struct SparseResponse {
457    /// Manifest committing to the delivery (104 bytes on the wire).
458    pub manifest: SparseManifest,
459    /// The subset of tree entries the filter selects, in canonical
460    /// lex-sorted order. The verifier walks these alongside the bitmap.
461    pub entries: Vec<TreeEntry>,
462    /// Proof bundle. Phase 2 carries only the raw bitmap bytes; the
463    /// future MMR proof slot is reserved for streaming-only transports.
464    pub proof: SparseProof,
465}
466
467/// Errors raised when encoding or decoding a [`SparseResponse`] on the
468/// wire. Kept tight — the transport layer wraps these in its own
469/// transport-error type at the call site.
470#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
471pub enum SparseWireError {
472    /// Buffer was shorter than the fixed-size manifest header (or
473    /// truncated partway through a length-prefixed section). The
474    /// decoder refuses to allocate from a truncated header.
475    #[error("sparse wire: truncated buffer")]
476    Truncated,
477    /// First 4 bytes are not `b"MSP1"`. Either the body is for a
478    /// different endpoint or the server is on a fork.
479    #[error("sparse wire: bad magic")]
480    BadMagic,
481    /// Version byte is something other than [`SPARSE_WIRE_VERSION`].
482    /// A future server speaking v2 must not be silently downgraded.
483    #[error("sparse wire: unsupported version {0}")]
484    UnsupportedVersion(u8),
485    /// One of the length-prefixed sections claims more bytes than
486    /// remain in the input. Likely a malicious server trying to make
487    /// us allocate.
488    #[error("sparse wire: length out of bounds")]
489    LengthOutOfBounds,
490    /// `leaf_count` exceeds [`MAX_LEAVES`]. The verifier MUST refuse
491    /// before allocating anything proportional to the claimed count.
492    #[error("sparse wire: leaf_count exceeds MAX_LEAVES")]
493    TooManyLeaves,
494    /// Encoded buffer would exceed [`SPARSE_WIRE_MAX_BYTES`].
495    #[error("sparse wire: response exceeds maximum size")]
496    TooLarge,
497    /// A tree entry's mode byte is not a valid [`EntryMode`].
498    #[error("sparse wire: invalid entry mode {0}")]
499    InvalidEntryMode(u8),
500    /// Entry name length declared as zero or > 255 bytes — outside the
501    /// SPEC-OBJECTS §4 range. Matches the per-tree-entry name cap.
502    #[error("sparse wire: invalid entry name length")]
503    InvalidEntryName,
504}
505
506/// Encode a [`SparseResponse`] to the canonical wire bytes.
507///
508/// Layout:
509/// ```text
510/// offset  size  field
511/// 0       4     magic        = SPARSE_WIRE_MAGIC ("MSP1")
512/// 4       1     version      = SPARSE_WIRE_VERSION
513/// 5       32    tree_hash
514/// 37      32    bitmap_root
515/// 69      32    filter_hash
516/// 101     8     leaf_count   (u64 LE)
517/// 109     4     entries_len  (u32 LE) — count of TreeEntry items
518/// 113     ...   TreeEntry stream, each entry:
519///                 u16 LE name_len   (1..=255)
520///                 name_len bytes    (name; arbitrary bytes)
521///                 u8          mode  (EntryMode)
522///                 [u8; 32]    object_hash
523/// ...     4     bitmap_len   (u32 LE)
524/// ...     N     bitmap_bytes
525/// ```
526///
527/// Names are u16-LE length-prefixed rather than the u32 used by
528/// SPEC-OBJECTS — names are bounded at 255 bytes, so a u16 prefix is
529/// already overkill, and the four-byte saving per entry adds up over a
530/// large tree. The decoder rejects `name_len == 0` and `name_len > 255`
531/// to keep the bound enforced.
532///
533/// # Errors
534///
535/// * [`SparseWireError::TooManyLeaves`] — `manifest.leaf_count > MAX_LEAVES`.
536/// * [`SparseWireError::InvalidEntryName`] — an entry name is empty or
537///   > 255 bytes. Defensive: callers should reject before encoding.
538/// * [`SparseWireError::TooLarge`] — the encoded buffer would exceed
539///   [`SPARSE_WIRE_MAX_BYTES`].
540pub fn encode_sparse_response(resp: &SparseResponse) -> Result<Vec<u8>, SparseWireError> {
541    if resp.manifest.leaf_count > MAX_LEAVES {
542        return Err(SparseWireError::TooManyLeaves);
543    }
544    // Pre-size: 113 byte header + per-entry (2 + name_len + 1 + 32) +
545    // 4-byte bitmap length prefix + bitmap bytes. Overshoot is fine.
546    let mut entries_size: usize = 0;
547    for e in &resp.entries {
548        if e.name.is_empty() || e.name.len() > 255 {
549            return Err(SparseWireError::InvalidEntryName);
550        }
551        entries_size = entries_size
552            .checked_add(2 + e.name.len() + 1 + 32)
553            .ok_or(SparseWireError::TooLarge)?;
554    }
555    let total = 113usize
556        .checked_add(entries_size)
557        .and_then(|n| n.checked_add(4))
558        .and_then(|n| n.checked_add(resp.proof.bitmap_bytes.len()))
559        .ok_or(SparseWireError::TooLarge)?;
560    if total > SPARSE_WIRE_MAX_BYTES {
561        return Err(SparseWireError::TooLarge);
562    }
563
564    let mut out = Vec::with_capacity(total);
565    out.extend_from_slice(&SPARSE_WIRE_MAGIC);
566    out.push(SPARSE_WIRE_VERSION);
567    out.extend_from_slice(&resp.manifest.tree_hash);
568    out.extend_from_slice(&resp.manifest.bitmap_root);
569    out.extend_from_slice(&resp.manifest.filter_hash);
570    out.extend_from_slice(&resp.manifest.leaf_count.to_le_bytes());
571
572    let entries_len = u32::try_from(resp.entries.len()).map_err(|_| SparseWireError::TooLarge)?;
573    out.extend_from_slice(&entries_len.to_le_bytes());
574    for e in &resp.entries {
575        // Name length already bounded above.
576        #[allow(clippy::cast_possible_truncation)]
577        let name_len = e.name.len() as u16;
578        out.extend_from_slice(&name_len.to_le_bytes());
579        out.extend_from_slice(&e.name);
580        out.push(e.mode as u8);
581        out.extend_from_slice(&e.object_hash);
582    }
583
584    let bitmap_len =
585        u32::try_from(resp.proof.bitmap_bytes.len()).map_err(|_| SparseWireError::TooLarge)?;
586    out.extend_from_slice(&bitmap_len.to_le_bytes());
587    out.extend_from_slice(&resp.proof.bitmap_bytes);
588
589    Ok(out)
590}
591
592/// Decode a wire-format sparse response. Refuses any input larger than
593/// [`SPARSE_WIRE_MAX_BYTES`] before parsing.
594///
595/// # Errors
596///
597/// * [`SparseWireError::Truncated`] — buffer ended mid-field.
598/// * [`SparseWireError::BadMagic`] — magic prefix mismatch.
599/// * [`SparseWireError::UnsupportedVersion`] — version byte mismatch.
600/// * [`SparseWireError::TooManyLeaves`] — claimed `leaf_count >
601///   MAX_LEAVES`. Refused before any allocation.
602/// * [`SparseWireError::LengthOutOfBounds`] — a length prefix exceeds
603///   the remaining buffer.
604/// * [`SparseWireError::InvalidEntryMode`] — an entry mode byte is
605///   not a recognised [`EntryMode`].
606/// * [`SparseWireError::InvalidEntryName`] — an entry name length is
607///   outside the 1..=255 SPEC-OBJECTS bound.
608pub fn decode_sparse_response(buf: &[u8]) -> Result<SparseResponse, SparseWireError> {
609    if buf.len() > SPARSE_WIRE_MAX_BYTES {
610        return Err(SparseWireError::TooLarge);
611    }
612    if buf.len() < 113 {
613        return Err(SparseWireError::Truncated);
614    }
615    if buf[0..4] != SPARSE_WIRE_MAGIC {
616        return Err(SparseWireError::BadMagic);
617    }
618    if buf[4] != SPARSE_WIRE_VERSION {
619        return Err(SparseWireError::UnsupportedVersion(buf[4]));
620    }
621    let mut tree_hash = [0u8; 32];
622    tree_hash.copy_from_slice(&buf[5..37]);
623    let mut bitmap_root = [0u8; 32];
624    bitmap_root.copy_from_slice(&buf[37..69]);
625    let mut filter_hash = [0u8; 32];
626    filter_hash.copy_from_slice(&buf[69..101]);
627    let mut leaf_count_bytes = [0u8; 8];
628    leaf_count_bytes.copy_from_slice(&buf[101..109]);
629    let leaf_count = u64::from_le_bytes(leaf_count_bytes);
630    if leaf_count > MAX_LEAVES {
631        return Err(SparseWireError::TooManyLeaves);
632    }
633
634    let mut entries_len_bytes = [0u8; 4];
635    entries_len_bytes.copy_from_slice(&buf[109..113]);
636    let entries_len = u32::from_le_bytes(entries_len_bytes) as usize;
637    // Independently bound by the per-tree cap.
638    if entries_len as u64 > MAX_LEAVES {
639        return Err(SparseWireError::TooManyLeaves);
640    }
641
642    let mut cursor: usize = 113;
643    let mut entries: Vec<TreeEntry> = Vec::with_capacity(entries_len.min(64));
644    for _ in 0..entries_len {
645        if cursor.checked_add(2).ok_or(SparseWireError::Truncated)? > buf.len() {
646            return Err(SparseWireError::Truncated);
647        }
648        let mut nlb = [0u8; 2];
649        nlb.copy_from_slice(&buf[cursor..cursor + 2]);
650        let name_len = u16::from_le_bytes(nlb) as usize;
651        cursor += 2;
652        if name_len == 0 || name_len > 255 {
653            return Err(SparseWireError::InvalidEntryName);
654        }
655        let needed = name_len
656            .checked_add(1)
657            .and_then(|n| n.checked_add(32))
658            .ok_or(SparseWireError::LengthOutOfBounds)?;
659        if cursor
660            .checked_add(needed)
661            .ok_or(SparseWireError::Truncated)?
662            > buf.len()
663        {
664            return Err(SparseWireError::Truncated);
665        }
666        let name = buf[cursor..cursor + name_len].to_vec();
667        cursor += name_len;
668        let mode_byte = buf[cursor];
669        cursor += 1;
670        let mode = EntryMode::from_u8(mode_byte)
671            .map_err(|_| SparseWireError::InvalidEntryMode(mode_byte))?;
672        let mut object_hash = [0u8; 32];
673        object_hash.copy_from_slice(&buf[cursor..cursor + 32]);
674        cursor += 32;
675        entries.push(TreeEntry {
676            name,
677            mode,
678            object_hash,
679        });
680    }
681
682    // Bitmap length prefix.
683    if cursor.checked_add(4).ok_or(SparseWireError::Truncated)? > buf.len() {
684        return Err(SparseWireError::Truncated);
685    }
686    let mut blb = [0u8; 4];
687    blb.copy_from_slice(&buf[cursor..cursor + 4]);
688    let bitmap_len = u32::from_le_bytes(blb) as usize;
689    cursor += 4;
690    if cursor
691        .checked_add(bitmap_len)
692        .ok_or(SparseWireError::LengthOutOfBounds)?
693        > buf.len()
694    {
695        return Err(SparseWireError::LengthOutOfBounds);
696    }
697    let bitmap_bytes = buf[cursor..cursor + bitmap_len].to_vec();
698    cursor += bitmap_len;
699    if cursor != buf.len() {
700        // Trailing bytes — refuse so a hostile server can't pad with
701        // extra data hoping a future client parses it.
702        return Err(SparseWireError::LengthOutOfBounds);
703    }
704
705    Ok(SparseResponse {
706        manifest: SparseManifest {
707            tree_hash,
708            bitmap_root,
709            filter_hash,
710            leaf_count,
711        },
712        entries,
713        proof: SparseProof { bitmap_bytes },
714    })
715}
716
717// ---------------------------------------------------------------------------
718// On-disk bitmap cache
719// ---------------------------------------------------------------------------
720
721/// Cache file header magic — `b"MSPC"` (mkit-sparse-cache). Distinct
722/// from the wire magic so a misnamed file can't be parsed as either.
723pub const SPARSE_CACHE_MAGIC: [u8; 4] = *b"MSPC";
724
725/// Cache file format version. Bumped on any breaking change.
726pub const SPARSE_CACHE_VERSION: u8 = 0x01;
727
728/// Subdirectory under `.mkit/` for the sparse bitmap cache.
729pub const SPARSE_CACHE_DIR: &str = "sparse";
730
731/// Encode the on-disk cache payload for a verified sparse delivery.
732///
733/// Layout:
734/// ```text
735/// 0   4   magic        = SPARSE_CACHE_MAGIC ("MSPC")
736/// 4   1   version      = SPARSE_CACHE_VERSION
737/// 5   32  bitmap_root
738/// 37  32  filter_hash
739/// 69  8   leaf_count   (u64 LE)
740/// 77  4   bitmap_len   (u32 LE)
741/// 81  N   bitmap_bytes
742/// ```
743///
744/// The `tree_hash` is *not* stored in the file body — it is the
745/// filename. Re-verifying a cached bitmap means recomputing the
746/// bitmap root from the bytes and comparing to `bitmap_root` here.
747#[must_use]
748pub fn encode_sparse_cache(manifest: &SparseManifest, proof: &SparseProof) -> Vec<u8> {
749    let mut out = Vec::with_capacity(81 + proof.bitmap_bytes.len());
750    out.extend_from_slice(&SPARSE_CACHE_MAGIC);
751    out.push(SPARSE_CACHE_VERSION);
752    out.extend_from_slice(&manifest.bitmap_root);
753    out.extend_from_slice(&manifest.filter_hash);
754    out.extend_from_slice(&manifest.leaf_count.to_le_bytes());
755    let len = u32::try_from(proof.bitmap_bytes.len()).unwrap_or(u32::MAX);
756    out.extend_from_slice(&len.to_le_bytes());
757    out.extend_from_slice(&proof.bitmap_bytes);
758    out
759}
760
761/// Decode the on-disk cache payload. Returns the bitmap root, filter
762/// hash, leaf count, and bitmap bytes — the caller reconstructs the
763/// `SparseManifest` if needed (the `tree_hash` field comes from the
764/// filename, not from the file body).
765///
766/// # Errors
767///
768/// Same shape as [`decode_sparse_response`].
769#[allow(clippy::type_complexity)]
770pub fn decode_sparse_cache(buf: &[u8]) -> Result<(Hash, Hash, u64, Vec<u8>), SparseWireError> {
771    if buf.len() < 81 {
772        return Err(SparseWireError::Truncated);
773    }
774    if buf[0..4] != SPARSE_CACHE_MAGIC {
775        return Err(SparseWireError::BadMagic);
776    }
777    if buf[4] != SPARSE_CACHE_VERSION {
778        return Err(SparseWireError::UnsupportedVersion(buf[4]));
779    }
780    let mut bitmap_root = [0u8; 32];
781    bitmap_root.copy_from_slice(&buf[5..37]);
782    let mut filter_hash = [0u8; 32];
783    filter_hash.copy_from_slice(&buf[37..69]);
784    let mut lcb = [0u8; 8];
785    lcb.copy_from_slice(&buf[69..77]);
786    let leaf_count = u64::from_le_bytes(lcb);
787    if leaf_count > MAX_LEAVES {
788        return Err(SparseWireError::TooManyLeaves);
789    }
790    let mut blb = [0u8; 4];
791    blb.copy_from_slice(&buf[77..81]);
792    let bitmap_len = u32::from_le_bytes(blb) as usize;
793    let end = 81usize
794        .checked_add(bitmap_len)
795        .ok_or(SparseWireError::LengthOutOfBounds)?;
796    if end > buf.len() {
797        return Err(SparseWireError::LengthOutOfBounds);
798    }
799    if end != buf.len() {
800        return Err(SparseWireError::LengthOutOfBounds);
801    }
802    let bitmap_bytes = buf[81..end].to_vec();
803    Ok((bitmap_root, filter_hash, leaf_count, bitmap_bytes))
804}
805
806// ---------------------------------------------------------------------------
807// Tests
808// ---------------------------------------------------------------------------
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use crate::hash::ZERO;
814    use crate::object::{EntryMode, TreeEntry};
815
816    fn entry(name: &[u8]) -> TreeEntry {
817        TreeEntry {
818            name: name.to_vec(),
819            mode: EntryMode::Blob,
820            object_hash: ZERO,
821        }
822    }
823
824    /// Tree with `n` lex-sorted entries named `b"aa"`, `b"ab"`,
825    /// `b"ac"`, etc. — enough variety that a prefix filter on
826    /// `"aa"` selects exactly one entry.
827    fn make_tree(n: usize) -> Tree {
828        assert!(n <= 26 * 26, "test helper only supports n <= 676");
829        let mut entries = Vec::with_capacity(n);
830        for i in 0..n {
831            // Two-letter ASCII names, lex-sorted by construction.
832            let a = b'a' + u8::try_from(i / 26).unwrap();
833            let b = b'a' + u8::try_from(i % 26).unwrap();
834            entries.push(entry(&[a, b]));
835        }
836        Tree { entries }
837    }
838
839    #[test]
840    fn build_and_verify_round_trip_simple() {
841        let tree = make_tree(10);
842        // Select "aa", "ab", "ac" — three distinct prefixes that
843        // each match exactly one entry (the entry names are 2 bytes
844        // and the filter paths are 2 bytes, so the exact-match arm
845        // of `entry_matches_filter` fires).
846        let filter = vec![
847            PathBuf::from("aa"),
848            PathBuf::from("ab"),
849            PathBuf::from("ac"),
850        ];
851
852        let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
853        assert_eq!(delivered.len(), 3);
854        assert_eq!(delivered[0].name, b"aa");
855        assert_eq!(delivered[1].name, b"ab");
856        assert_eq!(delivered[2].name, b"ac");
857        assert_eq!(manifest.leaf_count, 10);
858
859        assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
860    }
861
862    #[test]
863    fn verify_rejects_extra_entry() {
864        // Server tries to ship a 4th entry the filter didn't ask
865        // for. Even though the bitmap commits to the original 3,
866        // the count mismatch fires immediately.
867        let tree = make_tree(10);
868        let filter = vec![
869            PathBuf::from("aa"),
870            PathBuf::from("ab"),
871            PathBuf::from("ac"),
872        ];
873
874        let (mut delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
875        // Sneak in an entry that the filter does NOT select.
876        delivered.push(entry(b"ad"));
877
878        assert!(
879            !verify_sparse(&manifest, &delivered, &filter, &proof),
880            "verifier must reject delivered entries beyond the bitmap's set-bit count"
881        );
882    }
883
884    #[test]
885    fn verify_rejects_entry_outside_filter() {
886        // Server ships the right *number* of entries, but one of
887        // them isn't selected by the filter. The per-entry filter
888        // check fires.
889        let tree = make_tree(10);
890        let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
891
892        let (mut delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
893        assert_eq!(delivered.len(), 2);
894
895        // Replace "ab" with "az" — same count, but "az" isn't in
896        // the filter.
897        delivered[1] = entry(b"az");
898        assert!(
899            !verify_sparse(&manifest, &delivered, &filter, &proof),
900            "verifier must reject any delivered entry not selected by the filter"
901        );
902    }
903
904    #[test]
905    fn verify_rejects_tampered_bitmap_bytes() {
906        // Server flips a bit in `bitmap_bytes` (claims an extra
907        // entry was included) but doesn't update the manifest's
908        // bitmap_root. Root reconstruction catches it.
909        let tree = make_tree(10);
910        let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
911
912        let (delivered, manifest, mut proof) = build_sparse(&tree, &filter).unwrap();
913        // Flip a high bit nobody set.
914        proof.bitmap_bytes[0] ^= 0b1000_0000;
915
916        assert!(
917            !verify_sparse(&manifest, &delivered, &filter, &proof),
918            "verifier must reject when bitmap_bytes diverges from manifest.bitmap_root"
919        );
920    }
921
922    #[test]
923    fn verify_rejects_tampered_manifest_root() {
924        // Symmetric to the above: server preserves the bitmap but
925        // claims a different root. Catches an attacker substituting
926        // the manifest while leaving the bytes intact.
927        let tree = make_tree(10);
928        let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
929
930        let (delivered, mut manifest, proof) = build_sparse(&tree, &filter).unwrap();
931        manifest.bitmap_root[0] ^= 1;
932
933        assert!(!verify_sparse(&manifest, &delivered, &filter, &proof));
934    }
935
936    #[test]
937    fn verify_rejects_wrong_filter() {
938        // Manifest was built against filter A but verifier supplies
939        // filter B. `filter_hash` mismatch.
940        let tree = make_tree(10);
941        let filter_a = vec![PathBuf::from("aa")];
942        let filter_b = vec![PathBuf::from("ab")];
943
944        let (delivered, manifest, proof) = build_sparse(&tree, &filter_a).unwrap();
945        assert!(!verify_sparse(&manifest, &delivered, &filter_b, &proof));
946    }
947
948    #[test]
949    fn empty_filter_yields_empty_delivery() {
950        // Empty filter selects no entries. The manifest is
951        // well-defined (commits to "every bit unset") and verify
952        // accepts an empty delivered list.
953        let tree = make_tree(10);
954        let filter: Vec<PathBuf> = vec![];
955
956        let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
957        assert!(delivered.is_empty());
958        assert_eq!(manifest.leaf_count, 10);
959        assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
960    }
961
962    #[test]
963    fn empty_tree_is_well_defined() {
964        // No entries, no filter. Verifier accepts the trivial
965        // manifest.
966        let tree = Tree {
967            entries: Vec::new(),
968        };
969        let filter: Vec<PathBuf> = vec![];
970        let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
971        assert!(delivered.is_empty());
972        assert_eq!(manifest.leaf_count, 0);
973        assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
974    }
975
976    #[test]
977    fn prefix_filter_matches_subtree() {
978        // "src" should select "src/foo" and "src/bar" but NOT "srx"
979        // or "srcabc" (no `/` boundary).
980        let entries = vec![
981            entry(b"a"),
982            entry(b"src/bar"),
983            entry(b"src/foo"),
984            entry(b"srx"),
985        ];
986        // Lex-sort: 'a' < 'src/bar' < 'src/foo' < 'srx' — already sorted.
987        let tree = Tree { entries };
988        let filter = vec![PathBuf::from("src")];
989
990        let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
991        assert_eq!(delivered.len(), 2);
992        assert_eq!(delivered[0].name, b"src/bar");
993        assert_eq!(delivered[1].name, b"src/foo");
994        assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
995    }
996
997    #[test]
998    fn unsorted_tree_is_rejected() {
999        let tree = Tree {
1000            entries: vec![entry(b"b"), entry(b"a")],
1001        };
1002        let err = build_sparse(&tree, &[]).unwrap_err();
1003        assert_eq!(err, SparseError::UnsortedTree);
1004    }
1005
1006    #[test]
1007    fn filter_hash_is_order_independent() {
1008        // Canonical form sorts and dedups, so these must collide.
1009        let a = hash_filter(&[PathBuf::from("y"), PathBuf::from("x")]);
1010        let b = hash_filter(&[
1011            PathBuf::from("x"),
1012            PathBuf::from("y"),
1013            PathBuf::from("x"), // duplicate
1014        ]);
1015        assert_eq!(a, b);
1016        // ...but a different content set must produce a different hash.
1017        let c = hash_filter(&[PathBuf::from("x"), PathBuf::from("z")]);
1018        assert_ne!(a, c);
1019    }
1020
1021    // ----- Phase 2: canonical tree-hash + wire format ----------------------
1022
1023    #[test]
1024    fn tree_hash_matches_canonical_serialize_then_hash() {
1025        // The Phase-2 tree_hash is BLAKE3(serialize(Object::Tree(t))).
1026        // Cross-check against the codebase's canonical "address a tree
1027        // object" recipe — anything else would defeat the point of the
1028        // Phase-1 → Phase-2 swap.
1029        let tree = make_tree(5);
1030        let canonical = crate::hash::hash(
1031            &crate::serialize::serialize(&crate::object::Object::Tree(tree.clone())).unwrap(),
1032        );
1033        assert_eq!(tree_hash(&tree), canonical);
1034    }
1035
1036    #[test]
1037    fn tree_hash_differs_from_phase1_placeholder() {
1038        // Sanity: the Phase-1 sparse-internal hash and the new
1039        // canonical hash MUST differ for any non-empty tree, so a
1040        // mistakenly-pinned Phase-1 hash anywhere upstream surfaces
1041        // immediately as a verifier mismatch.
1042        let tree = make_tree(3);
1043        // Phase-1 placeholder recipe — kept verbatim for the diff.
1044        let mut body = Hasher::new();
1045        let count = u32::try_from(tree.entries.len()).unwrap();
1046        body.update(&count.to_le_bytes());
1047        for entry in &tree.entries {
1048            let name_len = u32::try_from(entry.name.len()).unwrap();
1049            body.update(&name_len.to_le_bytes());
1050            body.update(&entry.name);
1051            body.update(&[entry.mode as u8]);
1052            body.update(&entry.object_hash);
1053        }
1054        let body_digest = body.finalize();
1055        let phase1 = crate::hash::domain_digest(b"mkit-sparse-tree-v1", &body_digest);
1056        assert_ne!(tree_hash(&tree), phase1);
1057    }
1058
1059    #[test]
1060    fn wire_round_trip_simple() {
1061        let tree = make_tree(8);
1062        let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
1063        let (entries, manifest, proof) = build_sparse(&tree, &filter).unwrap();
1064        let resp = SparseResponse {
1065            manifest,
1066            entries,
1067            proof,
1068        };
1069        let bytes = encode_sparse_response(&resp).unwrap();
1070        let parsed = decode_sparse_response(&bytes).unwrap();
1071        assert_eq!(parsed.manifest, resp.manifest);
1072        assert_eq!(parsed.entries.len(), resp.entries.len());
1073        for (a, b) in parsed.entries.iter().zip(resp.entries.iter()) {
1074            assert_eq!(a.name, b.name);
1075            assert_eq!(a.mode as u8, b.mode as u8);
1076            assert_eq!(a.object_hash, b.object_hash);
1077        }
1078        assert_eq!(parsed.proof.bitmap_bytes, resp.proof.bitmap_bytes);
1079        // Decoded response still verifies.
1080        assert!(verify_sparse(
1081            &parsed.manifest,
1082            &parsed.entries,
1083            &filter,
1084            &parsed.proof
1085        ));
1086    }
1087
1088    #[test]
1089    fn wire_rejects_bad_magic() {
1090        let tree = make_tree(2);
1091        let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
1092        let mut bytes = encode_sparse_response(&SparseResponse {
1093            manifest,
1094            entries,
1095            proof,
1096        })
1097        .unwrap();
1098        bytes[0] = 0xFF;
1099        assert_eq!(
1100            decode_sparse_response(&bytes).unwrap_err(),
1101            SparseWireError::BadMagic
1102        );
1103    }
1104
1105    #[test]
1106    fn wire_rejects_unsupported_version() {
1107        let tree = make_tree(2);
1108        let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
1109        let mut bytes = encode_sparse_response(&SparseResponse {
1110            manifest,
1111            entries,
1112            proof,
1113        })
1114        .unwrap();
1115        bytes[4] = 0x99;
1116        assert!(matches!(
1117            decode_sparse_response(&bytes).unwrap_err(),
1118            SparseWireError::UnsupportedVersion(0x99)
1119        ));
1120    }
1121
1122    #[test]
1123    fn wire_rejects_trailing_garbage() {
1124        let tree = make_tree(2);
1125        let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
1126        let mut bytes = encode_sparse_response(&SparseResponse {
1127            manifest,
1128            entries,
1129            proof,
1130        })
1131        .unwrap();
1132        bytes.push(0xAA);
1133        assert_eq!(
1134            decode_sparse_response(&bytes).unwrap_err(),
1135            SparseWireError::LengthOutOfBounds
1136        );
1137    }
1138
1139    #[test]
1140    fn wire_rejects_truncated_header() {
1141        let tiny = vec![0u8; 50];
1142        assert_eq!(
1143            decode_sparse_response(&tiny).unwrap_err(),
1144            SparseWireError::Truncated
1145        );
1146    }
1147
1148    #[test]
1149    fn wire_rejects_overlong_leaf_count() {
1150        let mut buf = Vec::with_capacity(113);
1151        buf.extend_from_slice(&SPARSE_WIRE_MAGIC);
1152        buf.push(SPARSE_WIRE_VERSION);
1153        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1154        buf.extend_from_slice(&[0u8; 32]); // bitmap_root
1155        buf.extend_from_slice(&[0u8; 32]); // filter_hash
1156        buf.extend_from_slice(&(MAX_LEAVES + 1).to_le_bytes());
1157        buf.extend_from_slice(&0u32.to_le_bytes()); // entries_len = 0
1158        // need bitmap length prefix for completeness
1159        buf.extend_from_slice(&0u32.to_le_bytes());
1160        assert_eq!(
1161            decode_sparse_response(&buf).unwrap_err(),
1162            SparseWireError::TooManyLeaves
1163        );
1164    }
1165
1166    #[test]
1167    fn cache_round_trip_recovers_bitmap_root() {
1168        let tree = make_tree(10);
1169        let filter = vec![PathBuf::from("ab")];
1170        let (_entries, manifest, proof) = build_sparse(&tree, &filter).unwrap();
1171        let bytes = encode_sparse_cache(&manifest, &proof);
1172        let (bitmap_root, filter_hash, leaf_count, bitmap_bytes) =
1173            decode_sparse_cache(&bytes).unwrap();
1174        assert_eq!(bitmap_root, manifest.bitmap_root);
1175        assert_eq!(filter_hash, manifest.filter_hash);
1176        assert_eq!(leaf_count, manifest.leaf_count);
1177        assert_eq!(bitmap_bytes, proof.bitmap_bytes);
1178    }
1179
1180    #[test]
1181    fn cache_rejects_bad_magic() {
1182        let tree = make_tree(1);
1183        let (_entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
1184        let mut bytes = encode_sparse_cache(&manifest, &proof);
1185        bytes[0] = 0x00;
1186        assert_eq!(
1187            decode_sparse_cache(&bytes).unwrap_err(),
1188            SparseWireError::BadMagic
1189        );
1190    }
1191}