SPEC-DISCLOSURE.md
In docs/specs/
In docs/specs/
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 mkit 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 (MKDS, §8).
This is issue #1015 (verifier kit). Out of scope, deliberately: wasm bindings (PR 4), CLI commands (PR 5), the docs guide (PR 6).
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:
commit_id via an unbroken chain of BLAKE3/BMT checks (§3).A verified disclosure does not prove:
signer/signature_valid report whether the
commit's (or remix's) embedded Ed25519 signature verifies against its
embedded public key (SPEC-SIGNING §3). Binding that public key to a
person, organization, or trust level is application policy, exactly as
sign::verify_commit
already documents — this spec changes nothing about what a commit
signature covers.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 bytesEvery 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).
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 = "MKDP" (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;
there is no compatibility decoder.
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).
ChunkHdr carries an explicit chunk_idA 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.
A conformant verifier, given a trusted commit_id: Hash and an encoded
bundle: [u8]:
bundle outright if it exceeds MAX_BUNDLE_BYTES,
before decoding anything."MKDP" magic + version byte)
before decoding the codec body; an unrecognized version is a typed
error, not a fallback to another format.Vec bounded as in §3, and
MUST reject any trailing bytes after the declared body.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).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.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.steps.len() > MAX_TREE_DEPTH (128, SPEC-OBJECTS'
store::MAX_TREE_DEPTH).Step at index i (root first):name against SPEC-OBJECTS §4.1's entry-name
rules (TreeEntry::validate_name) —
an invalid name (trailing space, .mkit, a reserved Windows device
name, etc.) is rejected here, before its proof is even checked.domain_digest(b"mkit.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 mkit code beyond the public
domain constants.i is not the last step and
mode != Tree — every step but the last MUST descend into a
directory.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.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"mkit.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).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.
| 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.
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/mkit-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.
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.
childrenTwo 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":
Commit: tree_hash, plus parents only in history.Remix: tree_hash, plus parents only in history; never sources
(foreign-repo pointers, SPEC-OBJECTS §6).Tree: every entry object_hash.ChunkedBlob: every chunk.Tag: target.Blob / Delta: none (Delta.base_hash is not followed).The root, when present, MUST deserialize as a Commit, Remix, or
Tag. A typed error is raised otherwise.
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.
Encoded with commonware-codec conventions (fixed integers big-endian, lengths LEB128 varint, arrays raw), matching §3:
magic "MKCL" (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_536Decoders 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.
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.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.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.missing = [root], verified = 0.root differs; the manifest is a locator,
never a trust anchor.pack_key of the supplied packs, in
order, and the counts MUST match.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.| 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 |
rust/tests/golden/closure/ pins the bytes. 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/mkit-core/tests/golden_closure.rs reads only the committed
files.
MKDS proves a content range crossing chunk boundaries of one ChunkedBlob. It wraps unchanged MKDP v2 Range and Chunk bundles, rather than introducing a payload kind. External tools MUST be able to encode and verify MKDS from this section and §3–§5 alone. SPEC-HTTP-OBJECTS selects this representation by query.
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] = "MKDS"
version: u8 = 1
commit_id: [32]
offset: u64 absolute content offset
len: u64 content length, not inclusive endpoint
anchor: Vec<u8> encoded MKDP v2 Range
chunks: Vec<Vec<u8>> encoded MKDP v2 Chunk bundlesThe 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.
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 MKDS | 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 MKDP 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.
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, an MKDP or MKDS container supplied as the anchor, an MKDP
supplied in place of an MKDS, 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 MKDS wire version 1; MKDP wire version 2 and its existing vectors remain unchanged. No migration or compatibility reader is needed for the additive container.
payload_kind 3 is reserved for a future proof
that a name is absent from a Tree (a range proof over the two
lex-adjacent entries). Tracked as
#1027. Not
implemented here.| 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 |
0x00) packs are the
closure-profile carrier.