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}