Satsignal conformance — disclosure-v1

This is the conformance surface for the selective-disclosure spec (docs/notary_spec/disclosure-v1.md). It is the canonical "is this verifier / SDK / adapter conformant with satsignal.disclosure.v1?" reference for third-party verifier authors, SDK implementers, adapter authors, and standards-adjacency reviewers.

disclosure-v1.md is normative; this document is a flat checklist of the normative requirements with cross-references back to the relevant sections, the triggering input vector for each MUST, and the required verifier behavior. Per-profile preimage / canonicalization / leaf-id rules are normatively defined in the per-profile specs under docs/notary_spec/profiles/; this document binds those rules into the disclosure verifier contract by cross-reference. A verifier conformant with disclosure-v1 is conformant only for the specific profile literals it actually implements — partial-profile conformance is explicitly contemplated in §D below.

Relationship to sibling conformance docs

The three conformance docs cover three independent (but layered) surfaces:

Disclosure conformance is layered above bundle conformance: a verifier processing a disclosure bundle first satisfies the CONFORMANCE_bundle ordered checks against the disclosure .mbnt's own envelope (and against the original anchor's bundle when fetched or carried), THEN — if manifest.disclosure is present — additionally satisfies the §A–§E checks below. A verifier that does not implement disclosure rendering is still a conformant bundle-v1 verifier; it satisfies §A.1 of this document by tolerating the unknown manifest.disclosure key per bundle-v1.md §9.2 and parsing the rest of the bundle unchanged.

Normative-source pointer

docs/notary_spec/disclosure-v1.md is the normative master spec. Per-profile rules (CSV row / JSON field / text paragraph_sentence) bind into this document by cross-reference; the profile specs at docs/notary_spec/profiles/csv-row-v1.md, docs/notary_spec/profiles/json-field-v1.md, and docs/notary_spec/profiles/text-paragraph-sentence-v1.md are themselves normative for their own preimage / canonicalization / leaf-id / salting rules. This document is a flat checklist mapping each MUST in those sources to a triggering input vector and the required verifier output.

Conformance classes

ClassBound byScope
Disclosure-tolerant verifierbundle-v1.md §9.2; this doc §A.1parses bundles whose manifest.disclosure is present, but does not render the disclosure view
Disclosure-aware verifierdisclosure-v1.md §1§8; this doc §A–§E, §Fruns the full linked-anchor binding chain, per-revealed-leaf recompute, claims rendering, view-hash check
Fully-conformant verifieras above + all five v1 profilesadditionally implements all five v1 profile literals (CSV row + CSV column / JSON top-level-key + JSON deep-field / text) per §D.2
Partial-profile verifieras disclosure-aware + ≥ 1 v1 profileimplements a strict subset of the v1 profile literals; fails closed with unsupported_profile on the others (§D.3)

A verifier that runs the disclosure checks out of order, that silent-skips a missing fail code, or that downgrades a closed failure to a warning under any flag is not conformant.


§A — Manifest tolerance and discovery

Conformance items derived from disclosure-v1.md §2 (where disclosure lives in the bundle) and bundle-v1.md §9.2 (unknown-JSON-key tolerance).

A.1 — Tolerate manifest.disclosure when not implementing disclosure

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §2 post-table prose; bundle-v1.md §9.2A valid .mbnt whose manifest.json carries an additive top-level disclosure object, presented to a verifier that does NOT implement disclosure-v1.Skip the unknown disclosure key, parse the rest of the bundle exactly as if the key were absent, and complete bundle verification per CONFORMANCE_bundle. MUST NOT fail closed on the key's presence; MUST NOT treat the disclosure key as proof material.

A.2 — Run §A–§E checks in order on a disclosure-aware verifier

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §7 (numbered procedure)Any .mbnt whose manifest.disclosure is present, presented to a disclosure-aware verifier.Run the checks of §A.3, §B.1–B.5, §C.1–C.6, §D.1, §E.1–E.3 in the order given here (mirroring disclosure-v1.md §7 steps 1–5). The first failing check terminates the disclosure verification closed with the indicated fail code; subsequent checks MUST NOT be reported as "passed." Partial results (e.g. "3 of 5 revealed leaves verified") MUST NOT be reported as a success state per disclosure-v1.md §7 closing prose.

A.3 — Version literal pinning

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.1, §7 step 1A disclosure bundle whose manifest.disclosure.version is any string other than the exact literal "satsignal.disclosure.v1" (e.g. "satsignal.disclosure.v2", "satsignal.disclosure.V1", the empty string, or the field omitted).Fail closed with unsupported_disclosure_version. MUST NOT attempt a "best-effort" v1 parse of an unrecognized literal.

A.4 — Forever-prohibition: no original-anchor proofs.json carried

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 closing "Forever-prohibition" proseA disclosure bundle whose .mbnt archive contains a proofs.json of the original anchor (a proofs.json carrying the original document's full leaf-set, salts, or merkle leaves — distinct from any proofs.json of the disclosure bundle's own re-anchor if one exists). Triggering example: extract a valid disclosure bundle, copy the original .mbnt's proofs.json into the disclosure bundle's archive root, re-emit.Fail closed with original_proofs_carrier_forbidden. The bundle is malformed per the §4 forever-prohibition, which now mints this dedicated code (see disclosure-v1.md §4 closing prose). MUST NOT silently ignore the file; MUST NOT process its leaves; MUST NOT report the disclosure as verified.

§B — Linked-anchor binding chain

The five conformance items below map one-to-one to the five steps of the binding chain in disclosure-v1.md §4. A verifier MUST run them in order; the first failure terminates closed with the indicated code.

Triggering inputs are described as minimal mutations of a known-good disclosure bundle — concretely, of fixture G1 (text disclosure) or G2 (JSON disclosure) of §G, with the named field altered as described.

B.1 — Original-canonical-doc carrier missing

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 step 1From a valid disclosure bundle, delete the linked_anchor/canonical.json entry from the .mbnt archive, and run the verifier in offline-only mode (no /api/v1/proofs/<bundle_id> fetch permitted).Fail closed with linked_anchor_carrier_missing. (Online verifiers MAY instead fetch the original .mbnt via linked_anchor.bundle_id from /api/v1/proofs/<bundle_id> and extract its canonical.json; that fallback is implementation-defined and OUT of the offline conformance suite per disclosure-v1.md §4 step 1 closing note.)

B.2 — Carrier-to-on-chain commit mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 step 2From a valid disclosure bundle that includes the linked_anchor/canonical.json carrier, mutate any single byte of the carrier file (e.g. flip the last hex char of an inner digest, or change one byte of issuer). The carrier no longer JCS-canonicalizes to the bytes whose sha256 was committed at linked_anchor.txid.Fail closed with linked_anchor_canonical_hash_mismatch. The verifier MUST compute sha256(linked_anchor/canonical.json bytes as stored) and compare against the on-chain document_hash at linked_anchor.txid (resolved via /lookup_hash or any other bundle-v1-conformant verification path).

Note — on-chain commit well-formedness floor (defense-in-depth). Before performing the carrier-to-commit comparison, a conformant verifier MUST reject an on-chain document_hash that is absent, is not lowercase hex, or is shorter than 40 hex characters (the width of the 20-byte on-chain commitment), failing closed with the same linked_anchor_canonical_hash_mismatch code. This closes a vacuous-match hole: a verifier that compares only the commitment's leading width — e.g. sha256(carrier).slice(0, commit.length) === commit — would accept an empty or truncated commit (slice(0, 0) === "") and bind an arbitrary carrier to the chain. The floor is an additional trigger of the existing §B.2 code, not a new fail-code, so the §B/§C registries are unchanged. Reference implementations pin _MIN_ONCHAIN_COMMIT_HEX_LEN = 40 in both the Python (disclosure/verifier.py) and JS (verify-disclosure.mjs) verifier twins.

B.3 — Carrier root vs. disclosure.linked_anchor.root mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 step 3From a valid disclosure bundle, mutate manifest.disclosure.linked_anchor.root by flipping the last hex character (01). The carrier's subject.proofs.chunk_merkle.root is unchanged; the asserted linked_anchor.root no longer equals it.Fail closed with linked_anchor_root_mismatch. The verifier parses the carrier, reads subject.proofs.chunk_merkle.root, and compares against disclosure.linked_anchor.root.

B.4 — Carrier scheme vs. disclosure.linked_anchor.subject_profile mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 step 4From a valid disclosure bundle, mutate manifest.disclosure.linked_anchor.subject_profile to a different valid literal (e.g. change "satsignal.text.paragraph_sentence.v1" to "satsignal.json.field.v1"). The carrier's subject.proofs.chunk_merkle.scheme is unchanged.Fail closed with linked_anchor_profile_mismatch. (This is the linked-anchor-scope profile mismatch and is distinct from the per-leaf profile_mismatch of §C.3.) The verifier reads subject.proofs.chunk_merkle.scheme from the carrier and compares against disclosure.linked_anchor.subject_profile.

B.5 — Unsupported linked-anchor algo

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §4 step 5; profiles/csv-row-v1.md §5b, profiles/csv-column-v1.md §5b, profiles/text-line-v1.md §5b, profiles/json-keypath-v1.md §5b, profiles/json-ast-v1.md §5bThe accepted-algo set is: "sha256" for every implemented profile (incl. the native csv-column-v1) except the sealed-only native json-ast-v1, AND "merkle-hmac-sha256" (salt_version == "salt_v1") for every sealed-enabled native profile — all five: csv-row-v1 / csv-column-v1 / text-line-v1 / json-keypath-v1 / json-ast-v1 (csv-column-v1's sealed §5b uses the bare "chunk/" info; json-ast-v1's uses the scheme-prefixed "json-ast-v1/chunk/"). The native json-ast-v1 is sealed-ONLY: a standard (algo == "sha256") json-ast-v1 carrier is rejected at submit with scheme_requires_sealed (finer per-node granularity lowers per-leaf entropy), so its only accepted algo is "merkle-hmac-sha256". unsupported_linked_algo fires for any of: (a) a carrier subject.proofs.chunk_merkle.algo that is neither "sha256" nor "merkle-hmac-sha256" (e.g. "sha512", "blake3"); (b) the sealed algo "merkle-hmac-sha256" on a profile that is not sealed-enabled — a non-native profile (a salted dotted / json-field / sentence profile claiming it); (c) a sealed native carrier (algo == "merkle-hmac-sha256") whose chunk_merkle.salt_version is anything other than "salt_v1" (e.g. "salt_v2"), since salt_version is part of the sealed algo dispatch.Fail closed with unsupported_linked_algo. Sealed-mode native (algo == "merkle-hmac-sha256", salt_version == "salt_v1") is supported for csv-row-v1, csv-column-v1, text-line-v1, json-keypath-v1, and json-ast-v1 (the additive sealed lift — no version-literal bump; json-ast-v1 is sealed-ONLY); see profiles/csv-row-v1.md §5b / profiles/csv-column-v1.md §5b / profiles/text-line-v1.md §5b / profiles/json-keypath-v1.md §5b / profiles/json-ast-v1.md §5b and disclosure-v1.md §4.5. A carrier presenting the supported sealed pair does NOT fire this code — it proceeds to the §C per-leaf checks under the sealed HMAC leaf rule.

§C — Per-revealed-leaf verification

Items map to disclosure-v1.md §7 step 4 (per-revealed-leaf recompute) and §3.4 (merkle-proof invariants). A verifier MUST run these checks for each entry in disclosure.revealed[] before reporting the disclosure verified. Per §7 closing prose, partial verification ("3 of 5 leaves passed") MUST NOT be reported as a success state.

C.1 — Leaf-hash mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §7 step 4 (preimage recompute); per-profile §4 preimage rulesFrom a valid disclosure bundle, keep revealed[0].leaf_hash unchanged but mutate revealed[0].value by one byte (e.g. change a character of a text line, a CSV row, or a digit of a numeric JSON top-level-key entry — or de-canonicalize a JSON entry by adding a space after the colon, as in json_keypath_v1_native/N3_non_jcs_value_mistake). The published leaf_hash no longer matches the profile rule applied to the mutated value.Fail closed with leaf_hash_mismatch. The verifier MUST apply the per-profile leaf rule — for a native profile the bare sha256(utf8(value)) (csv-row-v1.md §4 / text-line-v1.md §4 / json-keypath-v1.md §4) or sealed HMAC(salt_b64, utf8(value)) (§5b), selected by the carrier chunk_merkle.algo; for a salted dotted profile the (profile, leaf_id, value, salt_b64) preimage (json-field-v1.md §4, text-paragraph-sentence-v1.md §7) — selected by revealed[i].profile — and compare its output against the published revealed[i].leaf_hash. For json-keypath-v1 the value is the canonical entry "key":jcs(value) (whitespace-free, NFC); a non-JCS entry recomputes to a different hash and fires this code.

C.2 — Merkle path mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.4 invariants 1–3; §7 step 4 proof-walkFrom a valid disclosure bundle, keep revealed[0].leaf_hash unchanged but mutate one byte of revealed[0].proof_path[0].hash (e.g. flip the last hex character). The proof-walk from leaf_hash no longer terminates at linked_anchor.root.Fail closed with merkle_path_mismatch. The verifier walks proof_path per §3.4: at each step it decodes the 64-char lowercase hex sibling and frontier to raw 32 bytes, concatenates `(sibling \\frontier) if side == "L" else (frontier \\sibling) to a 64-byte buffer, SHA-256s, and uses the result as the new frontier. After the final step the frontier MUST equal linked_anchor.root. Verifiers MUST NOT concatenate ASCII hex strings (per §3.4` raw-byte rule).

C.3 — Per-leaf profile mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.4 revealed[].profile row; §7 step 4 final sub-bulletFrom a valid disclosure bundle, mutate revealed[0].profile to a different valid literal while leaving linked_anchor.subject_profile unchanged (e.g. linked_anchor.subject_profile == "satsignal.csv.row.v1" but revealed[0].profile == "satsignal.json.field.v1").Fail closed with profile_mismatch. This is distinct from the linked-anchor-scope linked_anchor_profile_mismatch of §B.4: §C.3 fires when the disclosure-block-internal revealed[i].profile disagrees with the same block's linked_anchor.subject_profile; §B.4 fires when the disclosure-block's linked_anchor.subject_profile disagrees with the original anchor's carrier chunk_merkle.scheme.

C.4 — Invalid hash format

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.4 invariants 1 and 5A disclosure bundle in which any hash-typed field (linked_anchor.root, revealed[i].leaf_hash, any revealed[i].proof_path[j].hash, or presentation.view_sha256) fails to match ^[0-9a-f]{64}$. Triggering examples: uppercase hex (5D41402A…), partial-length (63 chars or 65 chars), base64, or any non-hex character.Fail closed with invalid_hash_format. The check applies uniformly to every hash-typed field in the disclosure block.

C.5 — View hash mismatch

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §7 step 5From a valid disclosure that includes a presentation block and a rendered artifact, mutate one byte of the rendered artifact's bytes as supplied to the verifier — the redacted copy delivered ALONGSIDE the .mbnt (the shipped bundle carries only manifest.json + linked_anchor/canonical.json; it does not embed the view) — e.g. change a character in the HTML view, or flip a byte in the canonical CSV view. The artifact's sha256 no longer equals presentation.view_sha256.Fail closed with view_hash_mismatch. The verifier sha256s the raw bytes of the supplied redacted copy and compares against disclosure.presentation.view_sha256. The check applies ONLY when disclosure.presentation is present; a disclosure without presentation has no view artifact to check (per disclosure-v1.md §5 Optional table). A verifier GIVEN the redacted copy MUST run the check; one that skips it (no copy supplied) MUST NOT report the presentation as verified.

C.6 — Sealed leaf missing salt

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §7 step 4 (sealed branch); profiles/csv-row-v1.md §5b.1, profiles/text-line-v1.md §5b.1, profiles/json-keypath-v1.md §5b.1A sealed native disclosure (csv-row-v1 / text-line-v1 / json-keypath-v1; carrier chunk_merkle.algo == "merkle-hmac-sha256", salt_version == "salt_v1") one of whose revealed leaves omits its salt_b64. This block is schema-valid: salt_b64 is structurally OPTIONAL for a native profile (the structural validator cannot see the carrier algo to know the leaf is sealed), so the missing salt passes the structural layer and reaches the §7 step 4 recompute. Triggering example: from a positive sealed bundle, delete revealed[0].salt_b64 entirely (fixtures csv_row_v1_native_sealed/.../S2_missing_salt, text_line_v1_native_sealed/.../S2_missing_salt, json_keypath_v1_native_sealed/.../S2_missing_salt).Fail closed with sealed_leaf_missing_salt. For the sealed native leaf rule the per-leaf HKDF salt is REQUIRED (csv-row-v1.md §5b.1 / text-line-v1.md §5b.1 / json-keypath-v1.md §5b.1); the verifier recomputes HMAC-SHA256(base64decode(salt_b64), utf8(value)), which cannot proceed without the salt. The verifier MUST NOT synthesize an empty / zero salt, MUST NOT skip the leaf, and MUST NOT report the disclosure verified. (This is distinct from the standard native rule, where salt_b64 is legitimately ABSENT — the bare sha256(utf8(value)) leaf needs none; §C.6 fires ONLY under the sealed algo.)

§D — Profile-literal handling

Items derived from disclosure-v1.md §7 step 3, §8 closing bullet, and §11 (profile registry pointer).

D.1 — Unsupported profile literal fails closed

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §7 step 3; §8 "Fail closed with unsupported_profile…"A disclosure bundle whose linked_anchor.subject_profile is a literal the verifier does not implement, e.g. "satsignal.email.sentence.v99" (a hypothetical future or unknown profile), or "satsignal.csv.row.v2" against a verifier that ships only v1 profiles.Fail closed with unsupported_profile. Silent skip is explicitly not a legal behavior per §7 step 3 — silently skipping would let a future bad-profile leaf appear "verified." The verifier MUST NOT render the leaf as verified without running the named profile's preimage rule.

D.2 — Five v1 profile literals MUST be recognized by a fully-conformant verifier

SourceRequired verifier behavior
disclosure-v1.md §11 profile registryA fully-conformant verifier (class "Fully-conformant verifier" above) MUST recognize the five native v1 profile literals — the live set that anchors actually commit and disclosures actually bind to — and apply the profile-specific preimage / leaf-id / canonicalization rule from each profile's spec when verifying revealed leaves:

The deprecated dotted literals — satsignal.csv.row.v1, satsignal.json.field.v1, satsignal.text.paragraph_sentence.v1 — are NOT part of the required live set. They are inert per disclosure-v1.md §11 (retained in the merkle-scheme allowlist forever and kept as a frozen regression-guard corpus, but no production flow emits or consumes them). A verifier MAY still recognize them to walk legacy bundles, but a fully-conformant verifier is not required to implement them; the live successors are csv-row-v1 (← satsignal.csv.row.v1), json-keypath-v1 (← satsignal.json.field.v1, top-level-key vs. deep RFC-6901 pointer), and text-line-v1 (← satsignal.text.paragraph_sentence.v1, line vs. sentence granularity).

Each profile's preimage / leaf-id construction / canonicalization is normatively defined in the profile spec, not in this document. A verifier that disagrees with the profile spec on any of these rules is non-conformant for that profile literal, regardless of whether the disclosure-v1 plumbing is implemented correctly.

D.3 — Partial-conformance disclosure

SourceRequired verifier behavior
disclosure-v1.md §7 step 3; §8 profile bulletA verifier that implements a strict subset of the five v1 profile literals (e.g. CSV row + JSON top-level-key but not text, CSV column, or JSON deep-field) MUST:

A disclosure bundle that involves more than one profile literal is out of scope for v1 — every revealed[i].profile MUST equal linked_anchor.subject_profile (per disclosure-v1.md §3.3 and §C.3 above), so a single disclosure bundle binds exactly one profile. A consumer verifying an audit packet that contains multiple disclosure bundles (each binding its own profile) MUST run the partial-conformance rule per bundle: any bundle whose profile is unsupported fails closed; bundles whose profile is supported verify normally.


§E — Claims rendering

Items derived from disclosure-v1.md §3.6 (claims contract) and §8 (verifier rendering behavior). The claims block is part of the cryptographic commitment (§6 JCS canonicalization) and a verifier that mis-renders it has broken the contract.

E.1 — Required does_not_prove codes present

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.6; required-codes listA disclosure bundle whose disclosure.claims.does_not_prove array is missing any of: incomplete_by_design, redacted_view_not_original, satsignal_does_not_certify. Triggering example: delete the {code: "incomplete_by_design", text: "…"} entry from the array.Fail closed with missing_claim_code. All three codes MUST be present in v1; anchorer-defined codes MAY be added alongside but the three required codes can NEVER be removed.

E.2 — Required proves codes present

SourceTriggering inputRequired verifier behavior
disclosure-v1.md §3.6; required-codes list (proves)A disclosure bundle whose disclosure.claims.proves array is missing leaf_set_membership or leaf_value_match; OR whose disclosure.presentation is present but whose proves array is missing presentation_integrity. (Conversely: a disclosure whose presentation is absent is conformant if proves lacks presentation_integrity — the precondition for that code is presentation presence.)Fail closed with missing_claim_code. The verifier MUST check the three required codes with presentation_integrity gated on the precondition.

E.3 — Verbatim display of does_not_prove text

SourceRequired verifier behavior
disclosure-v1.md §8 bullets 6 and "The verifier MUST NOT silently drop, rewrite, or summarize…"The verifier MUST display the text field of every entry in disclosure.claims.does_not_prove verbatim, in full array order, prominently next to the disclosure view. The verifier MUST NOT: (a) drop any entry; (b) re-word the displayed text (paraphrase, summarize, translate, simplify); (c) hide an entry behind a click / expand / "more info" affordance that is not visible by default; (d) render the code instead of the text. Anchorer-supplied text is part of the cryptographic commitment per §3.6 and §6; a verifier that suppresses or rewords it has broken the contract. Identical rules apply to the proves array's entries whose preconditions are met (per §8 bullet 5).

§F — Merkle-proof invariants

Items cross-reference disclosure-v1.md §3.4 (the merkle-proof invariants). The proof-path walk — the raw-byte fold, the L/R direction encoding, the empty-proof_path single-leaf case — is uniform and shared across every v1 profile and carrier. What is not uniform is the odd-node tree shape: that is a property of the original anchor's tree, encoded into the proof_path, and it differs by anchor. The legacy salted profiles (the deprecated dotted satsignal.csv.row.v1 and the salted JSON / text profiles) use promote-unchanged; the native csv-row-v1 profile (standard and sealed) uses duplicate-last (disclosure-v1.md §3.4). The verifier walks whatever siblings it is given and MUST NOT rebuild the root from an assumed odd-node convention — see §F.3.

F.1 — Raw-byte concatenation (positive vector)

SourcePositive test vectorRequired verifier behavior
disclosure-v1.md §3.4 invariant 1 ("hash algorithm") and post-prose ("Before concatenation, each 64-character lowercase hex digest MUST be decoded to its raw 32-byte value…")Use fixture C1 of text-paragraph-sentence-v1.md §11.1: two leaves with leaf_hash a0a9a9edaa1638eeb4122f3a295afa8138b8577364e8d921092b9c9f89a08aae (p0/s0) and 1bff79cb195d06b2602dc44ce1f7f8e4fc854e621d20954f7cf4fc47ad8e91e5 (p0/s1). Combining them per the spec's pinned rule (side: "R" from p0/s0's vantage) gives the merkle root ec0f6274cddd13fa394c0ba8f024b8b23184ccc946bc4cd01130d72cb5659094.The verifier MUST reproduce the published root from the raw-bytes concatenation rule. Concretely: decode both 64-char hex digests to their 32-byte values, concatenate to a 64-byte buffer in the `(p0/s0_raw \\p0/s1_raw) order, SHA-256 the buffer, re-encode to 64-char lowercase hex. Result MUST equal ec0f6274cddd13fa394c0ba8f024b8b23184ccc946bc4cd01130d72cb5659094`. A verifier that ASCII-concatenates the hex strings (producing a 128-character ASCII string) and SHA-256s that instead will produce a different digest and is non-conformant.

F.2 — Single-leaf tree (proof_path = [])

SourcePositive test vectorRequired verifier behavior
disclosure-v1.md §3.4 invariant 2Use fixture B6 of json-field-v1.md §8 (single-leaf tree from {"x": 10000000000}): one leaf at /x with leaf_hash = 8c1c6fc64c987c97126d6f89a673fcfd7ae8021dff940b56658cd9b36254a49f; the merkle root equals the leaf_hash directly, and any disclosure of this leaf carries proof_path = [].The verifier MUST accept an empty proof_path and check that the recomputed leaf_hash equals linked_anchor.root directly (no proof-walk iterations). MUST NOT inject a synthetic sibling or report merkle_path_mismatch on a legitimately-empty path; MUST NOT reject the disclosure for "proof_path too short."

F.3 — Odd-node behavior: the anchor rule, encoded in the proof_path (structure-agnostic walk)

The odd-node convention is not a verifier-implemented uniform rule — it is the original anchor's tree shape, baked into the proof_path. A conformant verifier is structure-agnostic: it walks the supplied proof_path (folding each {side, hash} sibling into the frontier per §3.4) and compares the final frontier to linked_anchor.root. It does NOT rebuild the root and does NOT assume any single odd-node convention. The two live conventions differ only in how the builder emitted the odd-promoted node; the walk of either is identical machinery.

A verifier that rebuilds the root from an assumed convention is the bug — not one that walks. Concretely it MUST NOT assume promote-unchanged and reject a self-sibling proof_path entry as spurious, and it MUST NOT assume duplicate-last and synthesize a self-sibling level where the proof_path skipped one. Both of the positive vectors below MUST verify.

Positive vector (a) — LEGACY salted, PROMOTE-UNCHANGED (1-entry path).

SourcePositive test vectorRequired verifier behavior
disclosure-v1.md §3.4 invariant 3 (legacy salted bullet); the frozen salted corpus tests/vectors/disclosure-v1/csv_row_v1/A1.fixture.jsonThe deprecated salted satsignal.csv.row.v1 fixture A1 (frozen at tests/vectors/disclosure-v1/csv_row_v1/A1.fixture.jsonnot csv-row-v1.md §8, whose §8 is now the native N1 below): three leaves (r000000 Alice, r000001 Bob, r000002 Carol). Level-0 pairs Alice+Bob into L1[0] = e7b134fe26e5e635dd4034eb61e01ff12a1762074733c919c7808d6a4d82df7d; Carol's L0[2] promotes unchanged to L1[1] = 083b5fa9f3666a62b29f911d81257f370314ebef4e9494eee3cbc73bc182f984. ROOT = SHA-256 of those two = 624152d7a5f8df218b275eb7561bdad6e630797e41821e6db621a9b26b01e538. The proof_path for revealing r000002 (Carol) is a single entry [{ "side": "L", "hash": "e7b134fe…df7d" }] (= L1[0]): the promoted node skipped level 0, so its path carries no level-0 sibling and folds Carol's leaf directly against L1[0] to reach the root.The verifier MUST walk the single-entry path and reach 624152d7…e538. It MUST NOT inject a synthetic self-sibling level (i.e. MUST NOT "fix" the path by duplicating Carol's leaf), and MUST NOT reject the 1-entry path as too short. (This is the legacy salted promote-unchanged convention; it is unchanged by the native reframe and remains valid for the deprecated dotted profile and the salted JSON / text profiles.)

Positive vector (b) — NATIVE csv-row-v1, DUPLICATE-LAST (2-entry self-sibling path).

SourcePositive test vectorRequired verifier behavior
disclosure-v1.md §3.4 invariant 3 (native duplicate-last bullet); csv-row-v1.md §6/§8 N1; frozen tests/vectors/disclosure-v1/csv_row_v1_native/N1.fixture.jsonThe native standard csv-row-v1 fixture N1 (csv-row-v1.md §6/§8): three bare-sha256 leaves (r000000 Alice 3147617d…800a, r000001 Bob 701287f2…9731, r000002 Carol f5edf8ce…988d). Level-0 pairs Alice+Bob into L1[0] = 4d6704c7c8fe0ad82fefbd7c7b530d8eb6087ff369568d6e763801ab9f07b5e6; Carol is the odd last node and duplicate-last self-pairs — `L1[1] = SHA-256(raw(Carol) \\raw(Carol)) = e49b438fe484c909f9795172f8aea598123c422bcf7e11f257c53ecd5875609d. ROOT = SHA-256 of those two = 19d82f92265bc904b4f356b1f69bb418e96bca56e57785d2d1ae7c1acc8d5e3e. The proof_path for revealing r000002 (Carol) is a **two-entry self-sibling path** [{ "side": "R", "hash": "f5edf8ce…988d" }, { "side": "L", "hash": "4d6704c7…b5e6" }]: the first entry is **Carol's own leaf hash** (the self-sibling that folds H(Carol\\Carol) = L1[1]), the second is L1[0] at level 1. The same duplicate-last shape applies to the native **sealed** carrier (S1, csv-row-v1.md §5b), whose Carol path is also a two-entry self-sibling path over HMAC leaves, AND to the native **text-line-v1** carriers (text_line_v1_native/N1, text_line_v1_native_sealed/S1), whose odd-last line l000002 is likewise a two-entry self-sibling path. The native **json-keypath-v1** carriers (json_keypath_v1_native/N1, json_keypath_v1_native_sealed/S1) share the **same** duplicate-last rule, though their 4-key example is **even** at every level (root 3c06af94…dbec8 standard / 8d8a2a6d…463c sealed) so no odd-last self-sibling arises in that fixture — the rule is identical, only the leaf count differs. Duplicate-last is shared by every native profile (csv-row-v1 / text-line-v1 / json-keypath-v1`, both modes).The verifier MUST walk the two-entry path — fold Carol's leaf against the self-sibling first entry, then against L1[0] — and reach 19d82f92…5e3e. It MUST NOT reject the self-sibling first entry as a spurious / duplicate sibling, and MUST NOT assume promote-unchanged (which would skip Carol's level-0 fold and miss the root). The convention is already in the path; the verifier just folds it.

Required behavior, precisely. The verifier MUST fold the supplied proof_path siblings (§3.4 raw-byte rule) and compare the final frontier to linked_anchor.root; it MUST verify both a promote-unchanged (skip-level, 1-entry) path and a duplicate-last (self-sibling, 2-entry) path correctly, because the convention is baked into the path it is handed. A verifier that hard-codes one convention and rebuilds the root will fail one of the two vectors above and is non-conformant.

F.4 — Leaf ordering: profile-defined, verifier does NOT re-sort

SourceRequired verifier behavior
disclosure-v1.md §3.4 invariant 4; per-profile ordering rulesThe merkle leaf-set order is profile-defined. The disclosure carries revealed[] entries in the order they appear in the leaf-set (which, per profile, is):

The verifier MUST NOT re-sort revealed[], MUST NOT impose a verifier-local sort (e.g. by leaf_id ASCII), and MUST walk each revealed leaf's proof_path from the published leaf_hash exactly as written. A verifier that re-sorts will compute proof-walk paths against the wrong sibling indices and will spuriously report merkle_path_mismatch on conformant disclosures.


§G — Positive conformance vectors

Two end-to-end "happy path" cases that a fully-conformant verifier MUST accept. These fixtures already exist in the profile specs; this section binds them into the disclosure verifier contract by reference.

G.1 — Text disclosure (cites fixture C15)

Source fixture: text-paragraph-sentence-v1.md §11.15 (C15 — the e.g. / i.e. multi-dot-abbreviation fixture).

Construction. Wrap C15 in a disclosure bundle whose manifest.disclosure is:

Expected verifier outputs at each §4 / §7 step:

  1. §7 step 1 — version literal matches "satsignal.disclosure.v1" → pass.
  2. §4 step 1 — carrier linked_anchor/canonical.json present → pass.
  3. §4 step 2 — sha256(carrier_bytes) == on_chain_document_hash → pass.
  4. §4 step 3 — carrier chunk_merkle.root == 65bc540…0ba == disclosure.linked_anchor.root → pass.
  5. §4 step 4 — carrier chunk_merkle.scheme == "satsignal.text.paragraph_sentence.v1" == disclosure.linked_anchor.subject_profile → pass.
  6. §4 step 5 — carrier chunk_merkle.algo == "sha256" → pass.
  7. §7 step 3 — subject_profile literal recognized by a fully-conformant verifier → pass.
  8. §7 step 4 (per-leaf, p0/s0) — preimage recompute via text-paragraph-sentence-v1.md §7 yields be173bfa…9882, matches published leaf_hash; proof-walk from leaf with one side: R sibling yields 65bc540…0ba, matches linked_anchor.root; revealed[0].profile == subject_profile → pass.
  9. §7 step 4 (per-leaf, p0/s1) — analogous walk yields 65bc540…0ba → pass.
  10. §7 step 5 — no presentation block, no view-hash check → not applicable.
  11. §8 claims rendering — three required does_not_prove codes present; required proves codes present (with presentation_integrity omitted by precondition) → pass.

Disclosure VERIFIED.

G.2 — JSON disclosure (cites fixture B1)

Source fixture: json-field-v1.md §8 (B1 — minimal {"name":"Alice","age":42}).

Construction. Wrap B1 in a disclosure bundle whose manifest.disclosure is:

Expected verifier outputs at each §4 / §7 step: mirror G.1's walk verbatim, substituting the B1 root and the json-field-v1.md §4 preimage rule. The preimage rule for revealed[0] is documented explicitly in json-field-v1.md §4.3 (48-byte preimage, sha256 → 533e320213e48d42a8a9472c8ad12739a576ab172fd126b632ad4f27a79ae687).

Disclosure VERIFIED.

G.3 — Native CSV standard disclosure (frozen fixture N1)

Source fixture: tests/vectors/disclosure-v1/csv_row_v1_native/N1.fixture.json the frozen native standard corpus; the inline values mirror profiles/csv-row-v1.md §4/§8 N1 (root 19d82f92265bc904b4f356b1f69bb418e96bca56e57785d2d1ae7c1acc8d5e3e).

Construction. A disclosure bundle over the native standard CSV anchor (name,age,role\nAlice,42,Engineer\nBob,35,Designer\nCarol,29,Writer):

Expected verifier outputs: the §4 binding chain passes (§4 step 5 admits algo == "sha256"); per-leaf §7 step 4 recomputes each leaf as the bare sha256(utf8(value)) (no salt read) and walks the duplicate-last proof path to the root. Disclosure VERIFIED.

G.4 — Native CSV sealed disclosure (frozen fixture S1)

Source fixture: tests/vectors/disclosure-v1/csv_row_v1_native_sealed/S1.fixture.json the frozen native sealed corpus; the inline values mirror profiles/csv-row-v1.md §5b (root 2207e09f1cafe3cb7099d905d47eef8c998d42a0b2413b3a0a0413110f47f6a3).

Construction. A sealed disclosure bundle over the same CSV anchored in sealed mode:

Expected verifier outputs: §4 step 5 admits ("csv-row-v1", "merkle-hmac-sha256") with salt_version == "salt_v1"; per-leaf §7 step 4 recomputes each leaf as HMAC-SHA256(base64decode(salt_b64), utf8(value)) under the published per-leaf salt and walks the duplicate-last proof path (including Carol's two-entry self-sibling path) to the root. Disclosure VERIFIED.

G.5 — Native TEXT standard disclosure (frozen fixture N1)

Source fixture: tests/vectors/disclosure-v1/text_line_v1_native/N1.fixture.json the frozen native standard text corpus; the inline values mirror profiles/text-line-v1.md §4/§8 N1 (root 5e4f6278d3e8f1a8175e8f635e76f828262e48b3f4e5b6ffd326357cc04a607c).

Construction. A disclosure over a native standard TEXT anchor (a .txt with a BOM, a blank line, and a trailing-whitespace line — canon drops the blank line and strips trailing whitespace → 3 non-empty-line leaves; NO header):

Expected verifier outputs: §4 binding passes (admits algo == "sha256"); per-leaf §7 step 4 recomputes the bare sha256(utf8(value)) and walks the duplicate-last path to the root. Disclosure VERIFIED.

G.6 — Native TEXT sealed disclosure (frozen fixture S1)

Source fixture: tests/vectors/disclosure-v1/text_line_v1_native_sealed/S1.fixture.json ; the inline values mirror profiles/text-line-v1.md §5b (root 0c108f30ce50f1348d572a9480806d6e0bf990971abb88e440206cd5175b0358).

Construction. A sealed disclosure over the same text anchored in sealed mode:

Expected verifier outputs: §4 step 5 admits ("text-line-v1", "merkle-hmac-sha256") with salt_version == "salt_v1"; per-leaf §7 step 4 recomputes HMAC-SHA256(base64decode(salt_b64), utf8(value)) and walks the duplicate-last path (including the odd-last two-entry self-sibling path) to the root. Disclosure VERIFIED.

G.7 — Native JSON standard disclosure (frozen fixture N1)

Source fixture: tests/vectors/disclosure-v1/json_keypath_v1_native/N1.fixture.json the frozen native standard JSON corpus; the inline values mirror profiles/json-keypath-v1.md §4/§6/§8 N1 (root 3c06af94a0af735cd19cc77349b7a962464cda703be16c66bbad2395913dbec8).

Construction. A disclosure over a native standard JSON anchor (the compact object {"name":"AcmeCorp","ssn":"123-45-6789","balance":1000,"public_id":42} → JCS canon, objects-only, sorted top-level keys balance,name,public_id,ssn → 4 leaves; NO header):

Expected verifier outputs: §4 binding passes (admits algo == "sha256"); per-leaf §7 step 4 recomputes the bare sha256(utf8(value)) over the canonical entry and walks the duplicate-last path to the root. Disclosure VERIFIED.

G.8 — Native JSON sealed disclosure (frozen fixture S1)

Source fixture: tests/vectors/disclosure-v1/json_keypath_v1_native_sealed/S1.fixture.json ; the inline values mirror profiles/json-keypath-v1.md §5b (root 8d8a2a6d5b0490b19501452a729a967e1306df3164d2e8fc129c9afd52cb463c).

Construction. A sealed disclosure over the same object anchored in sealed mode:

Expected verifier outputs: §4 step 5 admits ("json-keypath-v1", "merkle-hmac-sha256") with salt_version == "salt_v1"; per-leaf §7 step 4 recomputes HMAC-SHA256(base64decode(salt_b64), utf8(value)) and walks the duplicate-last path to the root. Disclosure VERIFIED.


§H — Test vector registry

Every fixture used in this document, with file pointers back to the profile-spec source.

Fixture handleSource fileSectionPurpose under this CONFORMANCE doc
C1 (text minimal)docs/notary_spec/profiles/text-paragraph-sentence-v1.md§11.1F.1 (raw-bytes concat positive vector); root ec0f6274…5094
C15 (text multi-dot abbreviations)docs/notary_spec/profiles/text-paragraph-sentence-v1.md§11.15G.1 (text-disclosure happy path); root 65bc540e…060ba
A1 (CSV minimal 3-row — DEPRECATED salted satsignal.csv.row.v1)tests/vectors/disclosure-v1/csv_row_v1/A1.fixture.json (frozen salted corpus)F.3 vector (a): legacy salted promote-unchanged positive vector (Carol = 1-entry path); root 624152d7…e538. NOT csv-row-v1.md §8 (whose §8 is the native N1 below).
B1 (JSON minimal)docs/notary_spec/profiles/json-field-v1.md§8G.2 (JSON-disclosure happy path); root c1f5e68c…e17c
B6 (JSON single-leaf)docs/notary_spec/profiles/json-field-v1.md§8F.2 (single-leaf-tree positive vector); leaf-and-root 8c1c6fc6…a49f
§3 example disclosuredocs/notary_spec/disclosure-v1.md§3Reference shape for §A.2 and the §B mutation-vector descriptions

The above are the profile-spec prose fixtures. In addition, two frozen JSON disclosure corpora for the native csv-row-v1 profile ship on disk and a conformance runner consumes them directly via tests/vectors/disclosure-v1/schema/fixture.schema.json. Each fixture pins expected.verify_ok (and, for negatives, the expected_fail_code / expected.fail_code); they bind the §B / §C / §D codes to concrete native payloads.

Native CSV — STANDARD corpus (tests/vectors/disclosure-v1/csv_row_v1_native/, algo: "sha256", unsalted):

Fixture handleFileMaps toExpected
N1 (CSV minimal 3-row — NATIVE csv-row-v1, duplicate-last)csv_row_v1_native/N1.fixture.json; profile source csv-row-v1.md §6/§8 N1§G.3 positive end-to-end; F.3 vector (b) (native duplicate-last self-sibling; Carol = 2-entry path); root 19d82f92…5e3everify_ok: true
N2_linked_anchor_profile_mismatchcsv_row_v1_native/N2_linked_anchor_profile_mismatch.fixture.json§B.4linked_anchor_profile_mismatch
N3_header_included_mistakecsv_row_v1_native/N3_header_included_mistake.fixture.json§C.2 (a header-included leaf-set yields a wrong path)merkle_path_mismatch
N1_leaf_hash_mismatchcsv_row_v1_native/negatives/N1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
N1_merkle_path_mismatchcsv_row_v1_native/negatives/N1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
N1_linked_anchor_root_mismatchcsv_row_v1_native/negatives/N1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
N1_linked_anchor_canonical_hash_mismatchcsv_row_v1_native/negatives/N1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch

Native CSV — SEALED corpus (tests/vectors/disclosure-v1/csv_row_v1_native_sealed/, algo: "merkle-hmac-sha256", salt_version: "salt_v1", per-leaf HKDF salts):

Fixture handleFileMaps toExpected
S1csv_row_v1_native_sealed/S1.fixture.json§G.4 positive end-to-end (HMAC leaf)verify_ok: true
S1_leaf_hash_mismatchcsv_row_v1_native_sealed/negatives/S1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
S1_wrong_saltcsv_row_v1_native_sealed/negatives/S1_wrong_salt.fixture.json§C.1 (swapped per-leaf salt)leaf_hash_mismatch
S1_merkle_path_mismatchcsv_row_v1_native_sealed/negatives/S1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
S1_linked_anchor_root_mismatchcsv_row_v1_native_sealed/negatives/S1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
S1_linked_anchor_canonical_hash_mismatchcsv_row_v1_native_sealed/negatives/S1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch
S2_missing_saltcsv_row_v1_native_sealed/negatives/S2_missing_salt.fixture.json§C.6 (sealed leaf lacking salt_b64)sealed_leaf_missing_salt
S3_wrong_salt_versioncsv_row_v1_native_sealed/negatives/S3_wrong_salt_version.fixture.json§B.5(c) (salt_version != "salt_v1")unsupported_linked_algo
S4_linked_anchor_profile_mismatchcsv_row_v1_native_sealed/negatives/S4_linked_anchor_profile_mismatch.fixture.json§B.4 (sealed carrier scheme == "csv-row-v2" != subject_profile)linked_anchor_profile_mismatch

Native TEXT — STANDARD corpus (tests/vectors/disclosure-v1/text_line_v1_native/, scheme: "text-line-v1", algo: "sha256", unsalted; NO header, empty lines dropped, duplicate-last):

Fixture handleFileMaps toExpected
N1 (text minimal 3-line — NATIVE text-line-v1, duplicate-last)text_line_v1_native/N1.fixture.json; profile source text-line-v1.md §4/§6/§8 N1§G.5 positive end-to-end; F.3 vector (b) shape (native duplicate-last self-sibling; l000002 = 2-entry path); root 5e4f6278…607cverify_ok: true
N2_linked_anchor_profile_mismatchtext_line_v1_native/N2_linked_anchor_profile_mismatch.fixture.json§B.4 (carrier scheme text-line-v2)linked_anchor_profile_mismatch
N3_empty_line_included_mistaketext_line_v1_native/N3_empty_line_included_mistake.fixture.json§C.2 (a blank-line-included leaf-set yields a wrong path — pins text-line-v1.md §3 drop-empties)merkle_path_mismatch
N1_leaf_hash_mismatchtext_line_v1_native/negatives/N1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
N1_merkle_path_mismatchtext_line_v1_native/negatives/N1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
N1_linked_anchor_root_mismatchtext_line_v1_native/negatives/N1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
N1_linked_anchor_canonical_hash_mismatchtext_line_v1_native/negatives/N1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch

Native TEXT — SEALED corpus (tests/vectors/disclosure-v1/text_line_v1_native_sealed/, scheme: "text-line-v1", algo: "merkle-hmac-sha256", salt_version: "salt_v1", per-leaf HKDF salts):

Fixture handleFileMaps toExpected
S1text_line_v1_native_sealed/S1.fixture.json§G.6 positive end-to-end (HMAC leaf); root 0c108f30…0358verify_ok: true
S1_leaf_hash_mismatchtext_line_v1_native_sealed/negatives/S1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
S1_wrong_salttext_line_v1_native_sealed/negatives/S1_wrong_salt.fixture.json§C.1 (swapped per-leaf salt)leaf_hash_mismatch
S1_merkle_path_mismatchtext_line_v1_native_sealed/negatives/S1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
S1_linked_anchor_root_mismatchtext_line_v1_native_sealed/negatives/S1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
S1_linked_anchor_canonical_hash_mismatchtext_line_v1_native_sealed/negatives/S1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch
S2_missing_salttext_line_v1_native_sealed/negatives/S2_missing_salt.fixture.json§C.6sealed_leaf_missing_salt
S3_wrong_salt_versiontext_line_v1_native_sealed/negatives/S3_wrong_salt_version.fixture.json§B.5(c)unsupported_linked_algo
S4_linked_anchor_profile_mismatchtext_line_v1_native_sealed/negatives/S4_linked_anchor_profile_mismatch.fixture.json§B.4 (sealed carrier scheme == "text-line-v2")linked_anchor_profile_mismatch

Native JSON — STANDARD corpus (tests/vectors/disclosure-v1/json_keypath_v1_native/, scheme: "json-keypath-v1", algo: "sha256", unsalted; JCS canon, objects-only, sorted top-level keys, NO header, duplicate-last):

Fixture handleFileMaps toExpected
N1 (JSON minimal 4-key — NATIVE json-keypath-v1, duplicate-last)json_keypath_v1_native/N1.fixture.json; profile source json-keypath-v1.md §4/§6/§8 N1§G.7 positive end-to-end; shares the native duplicate-last merkle (even 4-leaf example, no odd-last self-sibling in this fixture); root 3c06af94…dbec8verify_ok: true
N2_linked_anchor_profile_mismatchjson_keypath_v1_native/N2_linked_anchor_profile_mismatch.fixture.json§B.4 (carrier scheme json-keypath-v2)linked_anchor_profile_mismatch
N3_non_jcs_value_mistakejson_keypath_v1_native/N3_non_jcs_value_mistake.fixture.json§C.1 (a non-JCS entry — space after the colon — recomputes to a different hash; pins json-keypath-v1.md §4 the value is the canonical whitespace-free "key":jcs(value))leaf_hash_mismatch
N1_leaf_hash_mismatchjson_keypath_v1_native/negatives/N1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
N1_merkle_path_mismatchjson_keypath_v1_native/negatives/N1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
N1_linked_anchor_root_mismatchjson_keypath_v1_native/negatives/N1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
N1_linked_anchor_canonical_hash_mismatchjson_keypath_v1_native/negatives/N1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch

Native JSON — SEALED corpus (tests/vectors/disclosure-v1/json_keypath_v1_native_sealed/, scheme: "json-keypath-v1", algo: "merkle-hmac-sha256", salt_version: "salt_v1", per-leaf HKDF salts):

Fixture handleFileMaps toExpected
S1json_keypath_v1_native_sealed/S1.fixture.json§G.8 positive end-to-end (HMAC leaf); root 8d8a2a6d…463cverify_ok: true
S1_leaf_hash_mismatchjson_keypath_v1_native_sealed/negatives/S1_leaf_hash_mismatch.fixture.json§C.1leaf_hash_mismatch
S1_wrong_saltjson_keypath_v1_native_sealed/negatives/S1_wrong_salt.fixture.json§C.1 (swapped per-leaf salt)leaf_hash_mismatch
S1_merkle_path_mismatchjson_keypath_v1_native_sealed/negatives/S1_merkle_path_mismatch.fixture.json§C.2merkle_path_mismatch
S1_linked_anchor_root_mismatchjson_keypath_v1_native_sealed/negatives/S1_linked_anchor_root_mismatch.fixture.json§B.3linked_anchor_root_mismatch
S1_linked_anchor_canonical_hash_mismatchjson_keypath_v1_native_sealed/negatives/S1_linked_anchor_canonical_hash_mismatch.fixture.json§B.2linked_anchor_canonical_hash_mismatch
S2_missing_saltjson_keypath_v1_native_sealed/negatives/S2_missing_salt.fixture.json§C.6sealed_leaf_missing_salt
S3_wrong_salt_versionjson_keypath_v1_native_sealed/negatives/S3_wrong_salt_version.fixture.json§B.5(c)unsupported_linked_algo
S4_linked_anchor_profile_mismatchjson_keypath_v1_native_sealed/negatives/S4_linked_anchor_profile_mismatch.fixture.json§B.4 (sealed carrier scheme != subject_profile)linked_anchor_profile_mismatch

The profile-spec prose fixtures above are the existing inline material; this CONFORMANCE document does NOT mint new fixtures. The frozen native corpora are the on-disk frozen material captured against the profile rules; this registry binds them to the §A–§E codes by reference.


§I — Out-of-scope for v1 conformance

Per disclosure-v1.md §12, the following are explicitly out of scope for satsignal.disclosure.v1 conformance. A claim of conformance under this document does NOT imply any of:


§J — Followups (in-tree gaps)

Disclosure conformance has zero executable checks in-tree as of 2026-05-27 — there is no scripts/test_disclosure_*.py runner, no tests/vectors/disclosure-v1/ directory, and no scripts/run_vectors.py-equivalent entry that exercises a frozen disclosure .mbnt against the §A–§F checklist. Every fixture cited in §G and §H lives in a profile spec as prose; none have been captured as a frozen .mbnt. The categories below are tracked followups; adopters needing immediate executable coverage should supply their own against the cited profile fixtures.

  1. Disclosure-bundle fixture set. RESOLVED (block layer); partial-RESOLVED (archive layer). A frozen JSON fixture corpus at tests/vectors/disclosure-v1/ carries positive vectors (csv_row_v1/A1.fixture.json through A10 — the DEPRECATED salted satsignal.csv.row.v1 corpus, promote-unchanged, retained as an inert regression guard, distinct from the LIVE native csv-row-v1 vectors N1 (standard) + S1 (sealed) under csv_row_v1_native/ + csv_row_v1_native_sealed/; json_field_v1/B1 through B13; text_paragraph_sentence_v1/C1 through C15) and negative end-to- end vectors for every fail code in §A.3, §B.1–B.5, §C.1–C.6, §D.1, and §E.1–E.2 (one fixture per code under disclosure_end_to_end/negatives/). A stdlib-only conformance runner (Python or any other language) consumes these via tests/vectors/disclosure-v1/schema/fixture.schema.json and runs the §A–§E checklist against the corpus's disclosure_block + linked_anchor_carrier payloads. The fixture-corpus layer also satisfies the cross-language drift-prevention contract: any conformant implementation in any language proves itself by matching every fixture's expected_* byte-for-byte.

Still residual. A frozen .mbnt archive set (binary-zip format with manifest.json + canonical.json + optional proofs.json + optional rendered-view sidecar) is not yet in-tree. The corpus models the disclosure-block and carrier-bytes layers above the bundle envelope; archive-level checks for §A.4 (original_proofs_carrier_forbidden triggered by an unrevealed proofs.json in the zip) and §C.5 (view_hash_mismatch against the rendered artifact delivered with the archive) are not exercised by the JSON corpus and remain open as the residual §J.1 archive-layer gap. Adopters needing archive-layer coverage should ship their own .mbnt test archives wrapped around these fixtures' disclosure blocks.

  1. Positive G.1 / G.2 vector capture. The two happy-path vectors in §G are currently described as constructions over profile-spec fixtures (C15 / B1). Capturing them as concrete .mbnt bundles with a published canonical-doc carrier and a published on-chain document_hash (mock or real) would let scripts/test_legacy_bundles.py gain a disclosure sibling — scripts/test_disclosure_bundles.py — that walks §A–§E end-to-end against a frozen archive.
  2. Merkle-machinery positive vectors as standalone tests. §F.1 (raw-byte concat), §F.2 (single-leaf tree), §F.3 (odd-node walk — both the legacy salted promote-unchanged 1-entry path and the native csv-row-v1 duplicate-last 2-entry self-sibling path) are crisp arithmetic checks that could be exercised with a tiny stdlib script over the cited profile fixtures without needing a full .mbnt. A targeted scripts/test_merkle_invariants.py would close this gap quickly.
  3. §A.4 fail-code mint. RESOLVED. disclosure-v1.md §4's forever-prohibition on the original anchor's proofs.json riding in the disclosure bundle now mints a dedicated fail code: original_proofs_carrier_forbidden. The code is pinned in disclosure-v1.md §4 closing "Forever-prohibition" prose and cited as the required verifier output in §A.4 above. Verifier authors now have a stable identifier to surface for this case; no fallback to generic envelope-level "malformed bundle" is required.
  4. Multi-disclosure audit-packet conformance. §D.3 contemplates a consumer verifying an audit packet containing multiple disclosure bundles (each binding its own profile). The verifier behavior is implied by per-bundle conformance, but no audit-packet-level conformance contract is pinned in disclosure-v1.md. A follow-up spec (e.g. audit-packet-v1 revision) should pin the per-bundle roll-up rules.
  5. Claims-rendering executable check. §E.3 (verbatim display of does_not_prove text) is a UI invariant and cannot be exercised by a stdlib data-level test. A reference-implementation invariant in verifier.html (analogous to the §2.2 envelope-hardening enforcement at lines 1944–2111 cited in CONFORMANCE_bundle.md) would let adopters point at a known-correct implementation; a linter check would be even better. Currently both are absent.

Cross-references

Questions about this specification? Email hello@satsignal.cloud.