KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

SPEC-DISCLOSURE — partial-disclosure bundle and closure-profile verification

Status: Draft, normative. The wire format and verification algorithm below are pinned by the golden vectors in §6, §7.6, and §8.3 and are not expected to change incompatibly, but this document has not yet accumulated the implementation experience SPEC-CONVENTIONS §2.1 reserves stable for. Scope: proving that a single path, chunk, or byte range of a file belongs to a specific kit commit id, with proof bytes on the order of a few KiB regardless of repository size ("partial disclosure"), and proving that a served object set is the full content that commit id commits to ("closure profile" / full disclosure, §7), and a multi-chunk range container (KDSP, §8).

This spec covers the verifier kit. Out of scope, deliberately: CLI commands and the docs guide.

1. Purpose and trust model

A disclosure bundle lets an untrusted party (a data-availability provider, a light client, an auditor, a browser) hand a verifier three things — a trusted commit id, an encoded bundle, and nothing else — and receive back a Disclosed result whose payload bytes are exactly the content that commit id commits to, at the path the bundle discloses.

A verified disclosure proves:

A verified disclosure does not prove:

2. The authentication chain

commit id
  = BLAKE3(commit bytes)                          <- verifier recomputes; reads tree_hash
tree_hash
  = wrap(Tree, BMT(entry leaves))                 <- SPEC-MERKLE-OBJECTS §2/§3.2
    one Step per path component: entry + <= log2(M) siblings
child_id (a Step's disclosed leaf)
  |- Blob            (SPEC-OBJECTS §3):  full canonical bytes, or a Bao
  |                   slice over them (content offset o -> canonical
  |                   offset o + 10)
  \- ChunkedBlob      (SPEC-OBJECTS §7): wrap(ChunkedBlob, BMT([meta, chunks...]))
                       chunk step = chunk id at position (index + 1), via
                       one multi-proof over {0, index + 1} that also
                       authenticates total_size/chunk_size (SPEC-MERKLE-OBJECTS §3.1)
                       chunk bytes, or a Bao slice over the chunk's own
                       canonical bytes

Every layer verifies against the layer above and terminates at the trusted commit id; nothing in this chain is ever compared against a bare (pre-domain-wrap) BMT root (SPEC-MERKLE-OBJECTS §5.4).

3. Wire format

Encoded with commonware-codec conventions (SPEC-CONVENTIONS §3 generalizes; this corpus's specific convention, also used by transfer::PackListNode, is: fixed integers big-endian, every length an LEB128 varint, arrays raw, Option<T> as a bool discriminant followed by T when present):

magic   = "KDPB"              (4 raw bytes)
version: u8 = 2
commit_id: [u8; 32]
commit_bytes: Vec<u8>          <= 4 MiB
steps: Vec<Step>               <= MAX_TREE_DEPTH (128), root first
  Step {
    name: Vec<u8> (1..=255)     entry name, SPEC-OBJECTS §4.1 rules
    mode: u8                    EntryMode (SPEC-OBJECTS §4)
    child_id: [u8; 32]
    inner_root: [u8; 32]        bare BMT root of the parent Tree
    position: u32                BMT position (= entry index) in the parent Tree
    proof: Proof (max_items = 1) SPEC-MERKLE-OBJECTS §5.1/§5.2
  }
payload_kind: u8
  0  Object { bytes: Vec<u8> <= MAX_RAW_OBJECT_SIZE }
  1  Chunk  { total_size: u64, chunk_size: u32, index: u32,
              inner_root: [u8; 32], proof: Proof (max_items = 2),
              bytes: Vec<u8> <= MAX_RAW_OBJECT_SIZE }
  2  Range  { chunk: Option<ChunkHdr>,
              offset_in_blob: u64, len: u64, slice: Vec<u8>,
              chunk_len_proofs: Vec<LenProof> <= MAX_CHUNKS }
     ChunkHdr { total_size: u64, chunk_size: u32, index: u32,
                inner_root: [u8; 32], chunk_id: [u8; 32],
                proof: Proof (max_items = 2) }
     LenProof { index: u32, chunk_id: [u8; 32],
                proof: Proof (max_items = 1), slice: Vec<u8> }

Step.inner_root is the bare BMT root of the parent Tree this step's proof is verified against (the tree whose id is the commit's tree_hash for step 0, or the previous step's child_id). Chunk/ChunkHdr.inner_root is the bare BMT root of the ChunkedBlob whose id is the leaf id. LenProof entries reuse that same root (same object) and gain nothing.

The whole bundle MUST be <= MAX_BUNDLE_BYTES (64 MiB), checked before any decode work. Trailing bytes after the declared body MUST be rejected. version != 2, an unrecognized payload_kind, and an unrecognized mode byte are typed decode errors, never a bare "invalid". A version byte of 1 is a typed UnsupportedBundleVersion(1) error.

Step and ChunkHdr/LenProof's proof fields decode with merkle::Proof::decode's max_items bound (SPEC-MERKLE-OBJECTS §5.2): 1 for a single-leaf proof, 2 for a multi-proof over {0, position} (§4.3). Every Vec read is bounded with read_range before allocation (§5).

3.1 Why ChunkHdr carries an explicit chunk_id

A Bao SliceDecoder always requires its expected root hash up front — it cannot recover an unknown root from the slice bytes themselves, by construction (verified streaming's whole point is that the root is already trusted before decoding starts). For the Chunk payload kind the verifier can compute that hash itself, by hashing the disclosed bytes directly (they are the chunk's canonical bytes). For the Range payload kind on a ChunkedBlob leaf, only a byte range is disclosed — the verifier never sees the chunk's full bytes to hash — so the containing chunk's id MUST travel on the wire as its own field. ChunkHdr.chunk_id is exactly the value verify_chunk_with_meta's multi-proof authenticates against the leaf's ChunkedBlob id, so a forged chunk_id simply fails to fold to that id (§4.3) — carrying it explicitly costs nothing security-wise and closes an otherwise-unprovable gap.

4. Verification algorithm

A conformant verifier, given a trusted commit_id: Hash and an encoded bundle: [u8]:

  1. MUST reject bundle outright if it exceeds MAX_BUNDLE_BYTES, before decoding anything.
  2. MUST check the fixed 5-byte header ("KDPB" magic + version byte) before decoding the codec body; an unrecognized version is a typed error, not a fallback to another format.
  3. MUST decode the codec body with every Vec bounded as in §3, and MUST reject any trailing bytes after the declared body.
  4. MUST compare the bundle's embedded commit_id field against the externally trusted id the caller is verifying against, as a fast sanity check (a mismatch here is always also a hash mismatch at step 5, but checking the field first avoids hashing a large commit_bytes for an obviously-wrong bundle).
  5. MUST recompute BLAKE3(commit_bytes) and reject unless it equals the trusted commit_id — this is the one step that binds everything else to a value the caller did not have to fetch.
  6. MUST decode commit_bytes and accept only Commit or Remix (SPEC-OBJECTS §5/§6); any other decoded type is a typed error. Take tree_hash from whichever it is.
  7. MUST reject steps.len() > MAX_TREE_DEPTH (128, SPEC-OBJECTS' store::MAX_TREE_DEPTH).
  8. For each Step at index i (root first):
    1. MUST validate name against SPEC-OBJECTS §4.1's entry-name rules (TreeEntry::validate_name) — an invalid name (trailing space, .kit, a reserved Windows device name, etc.) is rejected here, before its proof is even checked.
    2. MUST check domain_digest(b"kit.tree\x00", inner_root) == expected_id first, where expected_id is the previous step's child_id (step 0 against tree_hash itself). A mismatch is a typed InnerRootMismatch. The field is never a second trust anchor (wrap-check-first, docs/VERIFY.md §5). MUST then require that the proof folds to exactly inner_root; a bundle whose proof folds to a different root than it declares is rejected even if the wrap of the declared root happens to match, and vice versa. MUST then wrap the folded value and compare to expected_id (SPEC-MERKLE-OBJECTS §5.4). A commonware-native verifier performs the wrap check itself and then runs upstream bmt::Proof::verify_element_inclusion against inner_root unmodified, needing no kit code beyond the public domain constants.
    3. MUST reject the whole bundle if i is not the last step and mode != Tree — every step but the last MUST descend into a directory.
  9. The authenticated path is steps.map(|s| (s.name, s.mode)), and the leaf id is the last step's child_id (or tree_hash itself when steps is empty — disclosing the root tree). A verifier compares this authenticated path against whatever path string it asked for; it MUST NOT trust a claimed path carried any other way.
  10. Dispatch on payload_kind:
    • Object: MUST deserialize bytes and check its content-address (BLAKE3, or the BMT root for a Tree/ChunkedBlob — SPEC-MERKLE-OBJECTS §2) equals the leaf id.
    • Chunk: MUST first check domain_digest(b"kit.chunked\x00", inner_root) == leaf_id, else reject with InnerRootMismatch. MUST then require that the multi-proof folds to exactly inner_root. MUST hash bytes (its BLAKE3 IS the chunk's Blob id, SPEC-OBJECTS §3) and verify, in one multi-proof over BMT positions {0, index + 1} against the leaf id, that (a) the metadata leaf the verifier computes itself from total_size/chunk_size and (b) that chunk hash at position index + 1 both fold to the leaf id (SPEC-MERKLE-OBJECTS §3.1, §5.5). The metadata leaf is never accepted as an externally supplied value at position 0 — only ever recomputed from the very fields being authenticated. A commonware-native verifier runs the wrap check and then upstream bmt::Proof::verify_multi_inclusion against inner_root unmodified.
    • Range, chunk = None (the leaf is a plain Blob): MUST reject len == 0. MUST verify a Bao slice proving len bytes at content offset offset_in_blob (Bao offset offset_in_blob + 10, SPEC-OBJECTS §3's prologue) against the leaf id. absolute_offset is offset_in_blob itself — nothing further to prove. chunk_len_proofs MUST be empty; a non-empty set here is a typed error (it has no target chunk to describe).
    • Range, chunk = Some(hdr) (the leaf is a ChunkedBlob): MUST reject len == 0. MUST apply the same wrap-check-first and fold-equals-declared rules as Chunk to hdr.inner_root. MUST run the same {0, index + 1} multi-proof as Chunk above (using hdr.chunk_id as the second leaf) against the leaf id. MUST verify a Bao slice proving len bytes at content offset offset_in_blob (Bao offset offset_in_blob + 10) against hdr.chunk_id. If hdr.index == 0, chunk_len_proofs MUST be empty — nothing precedes the first chunk for an entry to describe, so a non-empty set here is a typed error, exactly like the plain-Blob case above, never silently ignored; absolute_offset is then Some(offset_in_blob) (already absolute). Otherwise, if chunk_len_proofs is empty, absolute_offset is None (not requested). Otherwise, chunk_len_proofs MUST cover exactly the index set 0..hdr.index, with no gaps and no duplicates — an incomplete or malformed set MUST be rejected as a typed error, never silently treated as "absent". When it does cover 0..hdr.index completely, each entry's chunk id MUST verify (single-leaf proof, verify_chunk) against the leaf id at position index + 1, and each entry's slice MUST verify as a length proof (§4.1 below) against that chunk id; absolute_offset is then Some(sum of every proven length + offset_in_blob).

4.1 Length proofs

A length proof authenticates one chunk's declared content length without disclosing its bytes: a Bao slice over canonical bytes 0..10 (the fixed 6-byte prologue plus the 4-byte le32 length field, SPEC-OBJECTS §2/§3) against that chunk's own id, at Bao offset 0 (not +10 — this slice covers the header itself, not post-header content). The authenticated value is the le32 at bytes 6..10. Bao's own encoding header is not what is trusted here — only the canonical prologue bytes it wraps, which is exactly why the slice's target is the chunk's real content id, not a value Bao invents.

5. Bounds

Field Bound Rationale
whole bundle MAX_BUNDLE_BYTES = 64 MiB checked before any decode
commit_bytes 4 MiB comfortably above any real commit/remix
steps MAX_TREE_DEPTH = 128 SPEC-OBJECTS' existing tree-depth cap
Step.name 1..=255 bytes SPEC-OBJECTS §4.1
Object.bytes / Chunk.bytes MAX_RAW_OBJECT_SIZE = 1 GiB the store's existing object-size cap
Step/Chunk/ChunkHdr proof siblings max_items * MAX_LEVELS (32) SPEC-MERKLE-OBJECTS §5.2, before any sibling is read
chunk_len_proofs MAX_CHUNKS = 1,000,000 the store's existing per-manifest chunk-count cap
a single LenProof.slice 8 KiB proving 10 bytes needs one 1 KiB Bao leaf chunk plus at most MAX_LEVELS 64-byte parent hashes plus an 8-byte header — comfortably under 4 KiB; 8 KiB leaves headroom

A malicious leaf_count inside any embedded Proof cannot force a large allocation: sibling count is bounded by max_items * MAX_LEVELS before any sibling digest is read (SPEC-MERKLE-OBJECTS §5.2), and leaf_count itself is compared, never used to size an allocation.

6. Golden vectors

rust/tests/golden/disclosure/ (SPEC-CONVENTIONS §5) carries the authoritative pinned bytes: a deterministic fixture repo (fixed signer seed, fixed timestamp, fixed file bytes; a large file of a fixed PRNG stream so FastCDC yields several chunks), each <name>.bin disclosure bundle paired with a <name>.json sidecar recording the commit id, the requested path/selector, the expected Disclosed summary (path, leaf id, payload kind, offsets, disclosed bytes' BLAKE3), and the expected accept/reject outcome (with a reason for every reject). MANIFEST.txt pins every vector's BLAKE3 digest.

Accept vectors: the root tree; a shallow file; a 3-level-nested file; an executable-mode file; one chunk of a chunked file; a range inside a chunk (with and without chunk_len_proofs, the latter's absolute_offset checked against the fixture's known plaintext offset); a range covering a whole small blob (the first Bao block, and a last partial block).

Reject vectors: a non-Tree intermediate step mode; a step's proof checked against the wrong parent (two steps swapped); an invalid entry name (trailing space); a payload whose id does not equal the leaf id; a Chunk payload with a forged total_size (neg_chunk_meta_forged_total_size); a Range payload over a chunked leaf with a forged total_size in its chunk header (neg_range_chunk_meta_forged_total_size); a Bao slice at the wrong offset; a zero-length range on a plain-Blob leaf (neg_zero_length_range) and on a chunked leaf (neg_zero_length_range_chunked — len == 0 is checked before the chunk header's wrap/fold checks, per §4); an incomplete chunk_len_proofs set; a chunk_len_proofs entry present on a chunk-index-0 range (neg_len_proofs_on_chunk0); steps.len() = 129; version = 3; version = 1 (neg_bundle_version_1); a declared inner root that does not wrap to the parent id (neg_inner_root_forged); a declared inner root that wraps correctly but whose proof was built for a different tree (neg_inner_root_fold_mismatch); a trailing byte after the declared body; and commit_bytes that decode to a Tag (a valid, signed object — just not one with a tree_hash). Accept sidecars record step_inner_roots_hex and chunk_inner_root_hex.

rust/crates/kit-core/tests/golden_disclosure.rs reads only the committed files, runs verify::verify_disclosure, and compares the result against each sidecar; it never calls the fixture generator.

7. Closure profile (full disclosure)

Purpose: given a trusted commit (or remix, or tag) id and a set of object bytes — optionally wrapped in raw-only v1 packs plus a convenience manifest — a verifier checks that every object reachable from that id is present and re-hashes to the id it was supplied under. This is full disclosure against a commit id, for a DA provider, a light client, or an auditor. The root id is the only trust anchor. The manifest is an index of pack hashes; it does not authenticate the objects.

7.1 Modes and children

Two walk modes, because the on-chain anchor is per commit:

Mode Byte What is included
snapshot (default) 0 the root object and its tree closure (trees, blobs, chunked-blob manifests, chunks); tag → target; remix → tree. Parents are referenced but not included.
history 1 today's reachable_objects semantics: every ancestor, identical to what push/fetch ship

children(obj, mode) is the single source of truth for "what does this object reference":

The root, when present, MUST deserialize as a Commit, Remix, or Tag. A typed error is raised otherwise.

7.2 Raw-only pack rule

A closure pack is a SPEC-PACKFILE v1 pack of 0x00 (raw) entries only. The writer MUST NOT compress and MUST NOT emit deltas. This is so a wasm verifier, which is built without pack-zstd, can consume the profile: 0x03/0x04 entries error in that build via the existing stub, and a delta would require a store to resolve.

A verifier that sees a delta entry or a compressed entry MUST fail with a profile-violation error naming the pack index and entry index. It MUST NOT decompress in order to decide this — scan entry types first.

7.3 Manifest bytes

Encoded with commonware-codec conventions (fixed integers big-endian, lengths LEB128 varint, arrays raw), matching §3:

magic "KCLM"            (4 raw bytes)
version: u8 = 1
root: [u8; 32]
mode: u8                0 = snapshot, 1 = history
packs: Vec<[u8; 32]>    pack_key of each pack, in order; length <= 65_536

Decoders MUST reject trailing bytes, an unknown version, and an unknown mode. pack_key is BLAKE3 of the entire pack including its trailer (SPEC-PACKFILE §7).

The manifest is a convenience index. The root id remains the only trust anchor: a verifier that already has the packs MAY call the pack-set entry point directly and ignore the manifest.

7.4 Verifier MUSTs

  1. Re-hash every supplied object (deserialize then id_from_object). In the store-less map path, a deserialize failure is corrupt under the BLAKE3 of those bytes. A bit-flipped object that still deserializes is content-addressed under a different id and surfaces as missing (the referenced id) plus unreferenced (the supplied one), not corrupt. A verifier that fetches by id MAY report a hash mismatch as corrupt under the requested id; this is the useful classification when a store returns bytes from the requested object slot.
  2. Walk from the root with children(obj, mode) over the resulting id → bytes map or pull-based source (BFS, visited set). The map walk is store-less; a pull-based source fetches only requested ids.
  3. Anything referenced and absent is missing. Anything supplied and never visited is unreferenced when the verifier can enumerate the supplied set. Completeness fails on missing or corrupt; unreferenced is reported, not an error — a DA provider MAY serve a superset. A report that cannot enumerate the supplied set MUST set unreferenced_checked to false and leave unreferenced empty.
  4. The root itself missing is missing = [root], verified = 0.
  5. Delta or compressed pack entries are a profile violation (§7.2).
  6. The verifier MUST be given the trusted root by its caller and MUST reject a manifest whose root differs; the manifest is a locator, never a trust anchor.
  7. Manifest pack hashes MUST equal pack_key of the supplied packs, in order, and the counts MUST match.
  8. Object count MUST NOT exceed SPEC-PACKFILE MAX_ENTRIES (10,000,000). Pack payload sum and entry count reuse SPEC-PACKFILE caps. Tree depth, tree entries, chunks, and parents reuse store::MAX_TREE_DEPTH, serialize::MAX_TREE_ENTRIES, serialize::MAX_CHUNKS, serialize::MAX_PARENTS.

7.5 Bounds

Bound Value Applied to
pack entries 10,000,000 each pack, and the supplied object set
pack payload sum 4 GiB each pack (SPEC-PACKFILE §5)
raw object size 1 GiB each object (store cap)
manifest packs 65,536 packs vector
tree depth 128 inherited from object decode
tree entries / chunks 1,000,000 inherited from object decode
parents 1,000 inherited from object decode

7.6 Vectors

rust/tests/golden/closure/ pins the bytes. The vectors use their own small fixture repo. Its chunked.bin is a 36 KiB file split by FastCDC into more than one chunk. The fixture stores the ChunkedBlob directly, so the vectors stay under 1 MiB in total. Accept vectors: snapshot (fixture head commit), history (a second commit so history ≠ snapshot; the parent's tree objects are present only in history), tag_root (tag → commit, snapshot). Sidecars record the root, mode, pack hashes, expected verified count, and the sorted id list.

Reject / incomplete vectors: missing chunk; corrupt blob; unreferenced extra (still complete); a delta entry; a compressed entry; a manifest pack-hash mismatch; the wrong root id; a manifest whose root differs from the caller's trusted root; manifest version 2; a trailing byte on the manifest.

rust/crates/kit-core/tests/golden_closure.rs reads only the committed files.

scripts/golden/closure_ref.py is an independent check in Python. It shares no code with the Rust crates. It decodes the manifest and the packs, derives every object id, walks the closure from the trusted root, and compares the result with each sidecar.

8. Multi-chunk span container (KDSP v1)

KDSP proves a content range crossing chunk boundaries of one ChunkedBlob. It wraps unchanged KDPB v2 Range and Chunk bundles, rather than introducing a payload kind. External tools MUST be able to encode and verify KDSP from this section and §3–§5 alone. SPEC-HTTP-OBJECTS selects this representation by query.

8.1 Encoding and bounds

Fixed-width integers are big-endian; vector lengths are unsigned LEB128 varints as in §3 (minimal encoding, bounded to u32 for vector lengths). Arrays are raw bytes. No padding is inserted.

magic: [4] = "KDSP"
version: u8 = 1
commit_id: [32]
offset: u64                 absolute content offset
len: u64                    content length, not inclusive endpoint
anchor: Vec<u8>             encoded KDPB v2 Range
chunks: Vec<Vec<u8>>         encoded KDPB v2 Chunk bundles

The anchor MUST disclose content offset 0, length 1 of chunk first, with a complete preceding chunk_len_proofs set. For first = 0 that set MUST be empty; for first > 0 it MUST cover exactly 0..first. The chunk vector MUST contain 2–1,000,000 bundles, with consecutive indices first..last in ascending order, inclusive: for every adjacent pair, index[i+1] = index[i] + 1. Each contains the full canonical Blob bytes, including its prologue and length field.

A decoder MUST reject an encoded container larger than 64 MiB before any decoding or allocation. It MUST bound each vector length by the remaining input and 64 MiB, and chunk count to at most 1,000,000, before allocation. The verifier classifies a zero or one chunk count under §8.2. The decoder MUST reject trailing bytes, unknown magic or version, truncated fields, and invalid varints. A deployment SHOULD also cap requested content, for example at 8 MiB; this service cap is separate from format validity.

8.2 Verification and reject reasons

The caller supplies a trusted commit id; the embedded id MUST equal it. The verifier MUST perform the following checks in order and return a distinct reason for the first failed check. Reason labels below are the language-independent golden contract, not implementation type names.

Check Reject reason
Total encoded size at most 64 MiB span_too_large
Magic equals KDSP span_magic
Version equals 1 span_version
Fields and bounded vectors decode completely, including chunk count at most 1,000,000 span_encoding
No trailing bytes span_trailing_bytes
Embedded commit equals caller's trusted commit span_commit
Nonzero len and checked offset + len span_range_arithmetic
At least two chunk bundles span_chunk_count
Anchor exists and verifies under §4 against commit_id span_anchor_invalid
Anchor is a chunked Range at offset_in_blob 0 disclosing exactly one byte span_anchor_selector
Anchor has a complete preceding length-proof set and an authenticated absolute offset span_anchor_offset
Every chunk bundle verifies under §4 against commit_id span_inner_invalid
Every chunk payload is Chunk span_chunk_selector
Anchor and every chunk share authenticated path and leaf id span_leaf_context
Every bundle shares total_size, chunk_size, and chunk inner root span_chunk_context
Chunk indices are first..last, consecutive and in order (index[i+1] = index[i] + 1), starting at the anchor index span_chunk_order
Every Chunk's canonical bytes decode as a nonempty Blob; length comes from these verified bytes span_chunk_bytes
The first chunk's content begins with the anchor's disclosed byte and its canonical id equals the anchor's chunk id span_anchor_binding
Checked sum of lengths and anchor absolute offset fits total_size; requested [offset, offset+len) lies inside this span and starts inside the first chunk span_range_outside
The requested end exceeds the beginning of the last chunk (the last chunk is needed) span_last_unneeded

An incomplete nonempty length-proof set fails anchor verification with span_anchor_invalid; a valid KDPB anchor omitting its optional offset proofs at index > 0 fails with span_anchor_offset. The anchor's proven absolute offset is the beginning of the first included chunk, not first * chunk_size: content-defined chunking has variable lengths. Canonical Blob lengths, not the metadata chunk-size marker, determine all following boundaries. Checked arithmetic MUST be used throughout. span_chunk_context and span_anchor_binding are defence-in-depth checks: the authenticated object and chunk proofs make those failures unreachable once all earlier checks pass. They remain distinct reject reasons for an independent verifier that receives inconsistent decoded proof data.

On success the verifier MUST strip each Blob's canonical header, concatenate its content in order, and return the slice [offset - span_start, offset + len - span_start). It MUST also return the authenticated path, leaf id, trusted commit id, absolute offset, and signature verification result under §4's trust model. A claimed path outside the verified bundles is not evidence. The caller MUST compare that authenticated path and range against its request.

8.3 Vectors and version history

rust/tests/golden/http-objects/ pins accept containers starting at both zero and nonzero chunk indices, and rejects for gaps, duplicates, wrong selectors, mixed commits, mismatched leaves, missing/incomplete anchors, ranges outside the span, an unnecessary last chunk, magic/version errors, trailing bytes, truncation, a non-minimal varint, a varint above u32::MAX, a chunk count of 1,000,001, a KDPB or KDSP container supplied as the anchor, a KDPB supplied in place of a KDSP, and oversize input. Large reject bodies use sidecar byte-range copy and patch recipes over the accepted containers; each recipe reconstructs exact bytes before verification. The oversize vector uses a pinned small seed plus a sidecar expansion recipe to construct 64 MiB + 1 bytes without checking in a large zero file. The bad-varint vectors splice the malformed prefix over the real anchor length inside an otherwise complete valid container, and the chunk-count vector is zero-filled to one empty vector per declared chunk, so a lenient reader fails for a different reason and only the 1,000,000 cap rejects it. Each reject sidecar names the exact reason in §8.2.

Document version 4 adds KDSP wire version 1; KDPB wire version 2 and its existing vectors remain unchanged.

9. Later (out of scope for this document)

10. Invariants

Mutation Detected because
a span mixes commits, leaves, metadata, or nonconsecutive chunks the checks and distinct reject reasons in §8.2
a span claims incorrect boundaries an offset-proving anchor plus canonical chunk lengths (§8.2)
any disclosed byte tampered the Bao slice / BLAKE3 check against the authenticated id fails
a step's (name, mode, child_id) tampered that step's inclusion proof no longer folds to the expected parent id
declared inner root does not wrap to the parent id wrap-check-first (domain_digest(TYPE_DOMAIN, inner_root) == expected_id) fails with InnerRootMismatch
proof fold does not equal the declared inner root rejected even if the wrap of the declared root happens to match
step order/parent tampered (steps swapped) each step verifies against the previous step's child_id, not an independent root
a non-final step's mode misreported as non-Tree rejected outright (§4 step 8.3), before its proof is even checked
ChunkedBlob total_size/chunk_size forged the multi-proof's metadata leaf (computed by the verifier, never accepted as input) no longer folds to the leaf id
a Range payload's offset/length claim tampered the Bao slice fails to verify at the claimed offset
an incomplete/malformed chunk_len_proofs set rejected as a typed error, never silently treated as "no offset available"
a chunk_len_proofs entry present on a chunk-index-0 range rejected outright (UnexpectedLengthProofs), the same as the plain-Blob case — index 0 has nothing preceding it to describe
oversize bundle / proof sibling count / chunk-length-proof count bounded before any decode allocation (§5)
checked against a bare inner root instead of the object id every check here goes through the id-based merkle::verify_* (SPEC-MERKLE-OBJECTS §5.4)
a claimed path not matching what was actually authenticated callers compare the returned authenticated path, never a path string the bundle merely asserts
a closure object omitted the walk from the root reports it in missing; completeness fails
a closure object's bytes tampered map verification reports a deserialize failure as corrupt and a still-deserializable bit flip as missing plus unreferenced; a verifier that fetches by id MAY report a hash mismatch as corrupt under the requested id
a delta or compressed pack in a closure profile violation, before any decompress
a closure manifest pack hash swapped pack_key mismatch against the supplied pack
a closure verified against the wrong root the requested root is missing; the supplied set is unreferenced
a closure manifest whose root differs from the caller's trusted root typed ClosureRootMismatch; the manifest is a locator, never a trust anchor

Cross-references