satsignal.disclosure.v1 — selective disclosure of an anchored document

Versioning (2026-05-27). This is satsignal.disclosure.v1. The version literal inside the disclosure block is fixed at "satsignal.disclosure.v1". The shape evolves additively as v1.x: new fields are added as optional sub-keys of manifest.disclosure, every existing v1.0 disclosure block canonicalizes to identical bytes (when its v1.0 fields are unchanged), and unknown keys inside the disclosure block follow the existing bundle tolerance rule (bundle-v1.md §9.2). The on-chain canonical document and the bundle subtype are not touched. Breaking shape changes would ship as satsignal.disclosure.v2 carried in the same manifest.disclosure slot, alongside a new version literal — never as a quiet v1 mutation.

Terminology. Canonical vocabulary is proof / folder (full alias map: the compatibility map). A .mbnt bundle is the zip carrying manifest.json, canonical.json, optional proofs.json, optional sidecar. A disclosure is a partial-disclosure view of an original anchored document, carried in the manifest of a .mbnt bundle.

Status: draft 1, 2026-05-27. Audience: integrators who want to take an already-anchored document and later publish a redacted view of it with cryptographic proof that the revealed fragments are members of the original anchor; verifier authors who must render and check such a view. Goal: define exactly one shape for the disclosure block, the set of facts it commits to, and the membership-check contract a verifier runs against it — without changing the on-chain commitment, the canonical schema, or the bundle format.

Choosing a scheme — read before you redact. This format redacts within one anchored document: it requires the original anchor to carry a chunk_merkle proof over a supported chunk profile (csv-row-v1, text-line-v1, json-keypath-v1, … — registry in §11), and the disclosure proves the revealed chunks into that existing on-chain root. If your need is instead "anchor many discrete items, later reveal a subset", the manifest batch path (items[] of {label, sha256_hex} committed under one Merkle root, scheme manifest-items-v1) may fit with no redaction tooling at all — revealing one item is handing over its bytes plus its leaf position. The two leaf rules are not interchangeable: a manifest-items anchor is not a valid disclosure carrier (the redaction tool refuses carriers whose chunk_merkle.scheme it cannot list leaves for), and the disclosure profiles are not the manifest leaf rule — the manifest guide carries the mirror-image caveat. The choice happens at anchor time, not at disclosure time.

1. Why this exists

Satsignal already anchors a sha256 commitment of a client-supplied input and ships back a .mbnt bundle that lets any third party verify, fully offline, that the anchored bytes existed at the block time of the anchor's txid. That establishes a single fact: the anchorer held this exact document at time T1.

That fact alone is not enough when the anchorer later wants to publish part of the document — a few sentences from a contract, a few rows from a CSV, a few fields from a JSON record — and still let a verifier check that those revealed fragments came from the document that was anchored. The redacted view is a new artifact; its bytes are not the original document's bytes; recomputing sha256 of the view will never match the on-chain commitment.

satsignal.disclosure.v1 fills that gap. The anchorer (client-side) generates a redacted artifact and emits a disclosure block in the .mbnt manifest that lists the revealed leaves with the merkle proofs that bind them into the original anchor's leaf-set root. A verifier re-runs the original anchor verification AND walks each revealed leaf's proof path to the root committed in manifest.disclosure.linked_anchor.

What this spec deliberately does not do:

2. Where disclosure lives in the bundle

Prerequisite. Selective disclosure is only possible for original anchors that were committed under a leaf-set scheme — i.e. the original canonical doc carries subject.proofs.chunk_merkle.{scheme,algo,leaf_count,root}. A pure- byte_exact anchor (single hash, no leaves) cannot be selectively disclosed; an anchorer who anticipates future disclosure MUST anchor with a chunk_merkle proof from the start. This is a property of the original anchor, not of disclosure-v1: the disclosure spec cannot retrofit a leaf-set onto an anchor that was committed without one. Each profile spec names which chunk_merkle.scheme literal it emits, so an anchorer who knows which profile they intend to disclose under can pick the right profile at anchor time.

A disclosure bundle is an ordinary .mbnt. Its manifest.json carries an additive optional top-level key disclosure; everything else in the bundle is unchanged:

Verifier tolerance of an unknown top-level manifest key is the existing contract in bundle-v1.md §9.2: "Verifiers MUST tolerate unknown top-level keys in manifest.json … (skip and continue). The Satsignal service may add informational fields in any release; existing verifiers continue to operate." manifest.disclosure uses that surface as designed. A verifier that does not yet implement disclosure rendering parses the rest of the bundle exactly as it does today; a verifier that does implement disclosure rendering performs the additional checks defined in §7.

Four sub-decisions ride with this architectural choice: one bundle format, the receipt_kind lift, forever-contract profile literals, and the small initial profile set.

3. The object

The disclosure block is a single object under manifest.disclosure. A complete v1 example:

{
  "disclosure": {
    "version": "satsignal.disclosure.v1",
    "disclosure_id": "acmecorp-2026-Q2-excerpt-014",

    "linked_anchor": {
      "root":            "5d41402abc4b2a76b9719d911017c592d6f4a8a9c5f1c2b8e3d4a5b6c7d8e9f0",
      "txid":            "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "subject_profile": "satsignal.text.paragraph_sentence.v1",
      "bundle_id":       "01HZ7N3K2P5XJ9R8WQYV6T4MAB"
    },

    "revealed": [
      {
        "leaf_id":   "p3/s1",
        "profile":   "satsignal.text.paragraph_sentence.v1",
        "value":     "The supplier shall deliver no later than 2026-08-15.",
        "salt_b64":  "c2FsdC0wMDAxLWFiY2RlZmdoMTIzNDU2Nzg=",
        "leaf_hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
        "proof_path": [
          { "side": "R", "hash": "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9" },
          { "side": "L", "hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" },
          { "side": "R", "hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" }
        ]
      },
      {
        "leaf_id":   "p3/s4",
        "profile":   "satsignal.text.paragraph_sentence.v1",
        "value":     "Either party may terminate with thirty days' written notice.",
        "salt_b64":  "c2FsdC0wMDA0LXp5eHd2dXRzcnFwb25tbA==",
        "leaf_hash": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
        "proof_path": [
          { "side": "L", "hash": "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d" },
          { "side": "L", "hash": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" },
          { "side": "R", "hash": "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" }
        ]
      }
    ],

    "presentation": {
      "format":               "html",
      "view_sha256":          "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3",
      "redaction_marker":     "[REDACTED]",
      "structure_disclosure": "positions_preserved"
    },

    "claims": {
      "proves": [
        { "code": "leaf_set_membership",
          "text": "The revealed fragments listed in `revealed[]` were members of the leaf-set of the document anchored at `linked_anchor.txid` at block time T1." },
        { "code": "leaf_value_match",
          "text": "Each revealed fragment's content (`value`) matches the leaf the anchorer committed to under profile `linked_anchor.subject_profile`." },
        { "code": "presentation_integrity",
          "text": "If a presentation artifact is included, its bytes match `presentation.view_sha256`." }
      ],
      "does_not_prove": [
        { "code": "incomplete_by_design",
          "text": "This disclosure is incomplete by design; undisclosed content of the original document is not verified and may contain additional context, contradictions, or qualifications." },
        { "code": "redacted_view_not_original",
          "text": "The redacted presentation artifact is NOT the original document; its bytes will not match the on-chain anchor of the original." },
        { "code": "satsignal_does_not_certify",
          "text": "Satsignal does not certify completeness, faithfulness of selection, authorship of the original, or the legal effect of any revealed fragment." }
      ]
    }
  }
}

3.1 version

Literal "satsignal.disclosure.v1". A verifier that does not recognize the literal MUST fail closed with unsupported_disclosure_version (see §8). The literal is the disclosure-block analogue of the canonical schema literal — pinning it prevents a future v2 block from being silently parsed as v1.

3.2 disclosure_id

Opaque client-side identifier (string). Satsignal does not interpret it; it gives the anchorer a stable handle to reference this specific disclosure (in cover letters, audit logs, follow-up correspondence). It is required so that a verifier rendering the disclosure has something stable to display alongside it.

3.3 linked_anchor

Sub-object pointing at the original anchor whose document is being partially disclosed:

The linked_anchor block is the entire reason this spec exists: it is how the disclosure-package bundle reaches back to the original anchor without ever putting the link on-chain. Read the privacy posture in §9 before designing UI around it.

Three identifiers, read carefully. A reader walking a disclosure verification flow will encounter three different identifiers that look similar at a glance; they mean different things and a verifier MUST distinguish them:

A reader who sees bundle_id somewhere in a disclosure verification flow MUST check which of the three it is. Inside the disclosure block, linked_anchor.bundle_id ALWAYS means the ORIGINAL anchor's bundle.

3.4 revealed

Array of revealed-leaf records. Each entry:

Where a salt is present, it is not derivable from the document, which is the intended privacy property of a salted leaf-set: an attacker who guesses a candidate value cannot test it against an undisclosed leaf without also obtaining the salt. The unsalted standard csv-row-v1 rule deliberately trades that property away (the anchor-time choice of standard vs sealed) — see profiles/csv-row-v1.md §5 for the privacy posture.

``json { "side": "L", "hash": "<64-hex>" } ``

side is one of "L" or "R" and tells the verifier whether the sibling sits on the left or the right of the current frontier hash at that level. The verifier computes the next level by concatenating (sibling_hash || frontier) when side == "L" and (frontier || sibling_hash) when side == "R", then sha256-ing the result. The first entry's frontier is leaf_hash; the final iteration's output MUST equal linked_anchor.root. This direction encoding is pinned forever for v1 — a v2 disclosure would have to carry a different version literal to change it.

Before concatenation, each 64-character lowercase hex digest MUST be decoded to its raw 32-byte value. The verifier concatenates raw bytes (resulting in a 64-byte buffer), and SHA-256 hashes the resulting 64-byte buffer. Verifiers MUST NOT concatenate the ASCII hex strings; doing so produces a different (incorrect) digest.

Merkle proof invariants.

1. Hash algorithm. SHA-256, lowercase hex output, 64 characters. Any hash field not matching ^[0-9a-f]{64}$ fails closed with invalid_hash_format. 2. Single-leaf tree. When the original leaf-set contains exactly one leaf, the merkle root equals leaf_hash and proof_path is the empty array []. The verifier MUST accept an empty proof_path and check that the recomputed leaf_hash equals linked_anchor.root directly. 3. Odd-node behavior — the on-chain tree's rule governs; the walk is structure-agnostic. The odd-node rule that matters for membership is the rule the original anchor built its tree with, and the disclosure's proof_path encodes it. The verifier does not rebuild the root — it only walks proof_path, folding whatever siblings it is given (side/hash at each level) up to linked_anchor.root. The walk therefore verifies any odd-node convention correctly, because the convention is already baked into the sibling list: - Legacy salted profiles (the original salted disclosure trees built by the disclosure layer itself) use promote-unchanged: an unpaired node is lifted to the next level without re-hashing, and its proof_path simply skips that level (no sibling entry for the promoted step). This convention is unchanged for those profiles. - Native csv-row-v1 (standard and sealed) binds to the anchor's tree, which is duplicate-last-on-odd: an unpaired node's sibling is itself, and the builder emits a self-sibling entry ({ "side": "R", "hash": <the node's own hash> }) for the odd-promoted node — so the odd node's proof_path carries an extra entry rather than skipping a level. See profiles/csv-row-v1.md §6 for the worked self-sibling path.

A verifier MUST NOT assume a single odd-node convention and rebuild the root from it; it MUST walk the supplied proof_path. (The direction encoding and raw-byte concatenation are pinned forever for v1; a v2 disclosure carrying a different version literal would be required to change those.) 4. Leaf ordering. The leaf-set order is profile-defined and the profile spec MUST pin a total order over leaves (e.g. document-order for text profiles, row-index for csv, JSON Pointer lexicographic for json-field). The disclosure carries leaves in the order they appear in the leaf-set; the verifier does NOT re-sort. 5. Invalid hex / case / length. Any hash, root, or leaf_hash field that is not exactly 64 lowercase hex characters fails closed with invalid_hash_format. Uppercase, partial- length, base64, and any non-hex characters are rejected.

3.5 presentation

Sub-object describing the generated redacted artifact, if one is produced with the disclosure. Fields:

3.6 claims

Sub-object with exactly two arrays, proves and does_not_prove. Each entry is a {code, text} pair: code is a machine-stable identifier, text is the human-readable prose displayed alongside the disclosure. Both arrays are part of the cryptographic commitment — they are JCS- canonicalized into the manifest bytes — and a verifier MUST display the text of every entry verbatim (§8). This is what protects against verifier mis-attribution: if a third party later argues "this disclosure proves authorship / completeness / legal effect," the disclosure itself carries an explicit, on-record refutation.

Codes are the forever-contract; text is anchorer-supplied. The set of code literals is the v1 contract — splitting code from text lets the protocol pin the meaning without locking the wording, so an anchorer can adapt the displayed copy to their audience (plain language, translated copy, domain-specific phrasing) without breaking the contract.

4. Binding linked_anchor.root to the original anchor

The disclosure block asserts that linked_anchor.root is the merkle root the revealed leaves must prove into. That assertion is worthless on its own: a malicious disclosure could merkle-prove leaves into an arbitrary root, with no on-chain binding back to any real document. This section pins the binding chain the verifier MUST walk before treating linked_anchor.root as authoritative.

The binding chain (plain prose). A verifier MUST prove, in order:

revealed[i].valuerevealed[i].leaf_hash (via the profile's leaf-hash rule — a salted preimage for salted carriers, a bare sha256(utf8(value)) for the standard native csv-row-v1 carrier) → linked_anchor.root (via proof_path walk) → original canonical-doc subject.proofs.chunk_merkle.root (MUST equal) → on-chain document_hash at linked_anchor.txid (via canonical-doc sha256) → BSV block timestamp at linked_anchor.txid (the T1 fact).

Any break in the chain fails the disclosure closed.

The original-canonical-doc carrier. The disclosure bundle SHOULD include a copy of the original anchor's canonical-doc bytes inside the .mbnt, under the filename linked_anchor/canonical.json. This file is the JCS-canonicalized bytes of the original canonical.json (verbatim — the exact bytes that were sha256'd into the on-chain document_hash at T1). When present, it allows fully offline binding verification: the verifier sha256s these bytes and compares against the on-chain document_hash, then reads subject.proofs.chunk_merkle.root from them and confirms it equals disclosure.linked_anchor.root.

Why carrying it does not leak the original. The original canonical document, by design, carries only digests / roots / metadata — it never contains the document's actual content (per bundle-v1.md §4: "leaves never enter the canonical doc; they ride off-chain in proofs.json"). Carrying canonical.json therefore reveals the original's anchor metadata (issuer, issued_at, leaf_count, profile scheme, merkle root) but not the unrevealed leaves. The unrevealed proofs.json of the original anchor MUST NOT be carried by the disclosure bundle.

The verifier procedure. A conforming verifier MUST run these five checks in order; any failure terminates closed with the indicated code.

  1. Locate the carrier. Locate linked_anchor/canonical.json inside the disclosure .mbnt. If absent and the verifier is operating offline-only, fail closed with linked_anchor_carrier_missing. (Online verifiers MAY instead fetch the original .mbnt via linked_anchor.bundle_id from the /api/v1/proofs/<bundle_id> endpoint and extract its canonical.json; this is implementation- defined and OUT of the offline conformance suite.)
  2. Bind carrier to on-chain commit. Compute sha256(linked_anchor/canonical.json bytes as stored). This MUST equal the on-chain document_hash at linked_anchor.txid (resolved via /lookup_hash or any other Satsignal-spec verification path). Mismatch → fail closed with linked_anchor_canonical_hash_mismatch.
  3. Bind carrier to disclosure root. Parse the canonical doc and read subject.proofs.chunk_merkle.root. Confirm it equals disclosure.linked_anchor.root. Mismatch → fail closed with linked_anchor_root_mismatch.
  4. Bind carrier to disclosure profile. Read subject.proofs.chunk_merkle.scheme. Confirm it equals disclosure.linked_anchor.subject_profile. Mismatch → fail closed with linked_anchor_profile_mismatch. (This replaces the existing profile_mismatch code for this specific check; the per-leaf profile_mismatch from §7 — comparing revealed[i].profile against linked_anchor.subject_profile — still applies.) For a native anchor this scheme is a hyphenated literal — "csv-row-v1" for CSV, "text-line-v1" for text, or "json-keypath-v1" for JSON, the values standard and sealed anchors stamp into chunk_merkle.schemenot a deprecated dotted profile (satsignal.csv.row.v1, satsignal.text.paragraph_sentence.v1, satsignal.json.field.v1); the disclosure binds to the chunk_merkle the anchor already committed, with no re-anchor and no new scheme. See §11.
  5. Pin the merkle algorithm. Read subject.proofs.chunk_merkle.algo. For a native carrier (csv-row-v1 / text-line-v1 / json-keypath-v1) a conforming verifier accepts two algos, each selecting a leaf-hash rule (§7 step 4):

Both branches are part of the v1.x contract; the merkle-hmac-sha256 branch is the additive sealed lift (no version-literal bump, no on-chain or canonical-schema change, no new bundle subtype). Any algo a verifier does not implement (anything other than these two for a native profile, the sealed algo on a non-native profile, or a salted-dotted profile's own algo) → fail closed with unsupported_linked_algo.

Forever-prohibition: no proofs.json of the original. The disclosure bundle MUST NOT carry the original anchor's proofs.json (or any other file that would reveal unrevealed leaves of the original). The whole privacy claim of selective disclosure rests on this: a disclosure that ships the original's proofs.json has revealed every leaf, defeating the selective property. A bundle that carries it is malformed. A disclosure-aware verifier that encounters such a bundle MUST fail closed with original_proofs_carrier_forbidden.

5. Required vs optional fields

Required

FieldTypeNotes
disclosure.versionstring literalMUST be exactly "satsignal.disclosure.v1".
disclosure.disclosure_idstringOpaque client-side identifier. Must be non-empty.
disclosure.linked_anchorobjectSub-fields below are all required.
disclosure.linked_anchor.roothex string64 lowercase hex chars.
disclosure.linked_anchor.txidhex string64 lowercase hex chars.
disclosure.linked_anchor.subject_profilestringA profile literal in the registry (§11).
disclosure.linked_anchor.bundle_idstringOpaque pointer to the original proof.
disclosure.revealedarrayMUST be non-empty. Each entry's sub-fields below are all required.
revealed[i].leaf_idstringProfile-defined.
revealed[i].profilestringMUST equal linked_anchor.subject_profile.
revealed[i].valuestring or structProfile-defined; verifier hashes it per the profile leaf-hash rule.
revealed[i].leaf_hashhex string64 lowercase hex chars.
revealed[i].proof_patharrayOrdered {side, hash} entries; MAY be empty only when the leaf-set was a single leaf (root == leaf_hash).
disclosure.claimsobjectBoth proves and does_not_prove sub-arrays required.
disclosure.claims.provesarray<{code,text}>MUST include the v1 required codes (leaf_set_membership, leaf_value_match, and — when presentation is present — presentation_integrity). Each entry's text is anchorer-supplied; defaults RECOMMENDED.
disclosure.claims.does_not_provearray<{code,text}>MUST include all three v1 required codes: incomplete_by_design, redacted_view_not_original, satsignal_does_not_certify. Each entry's text is anchorer-supplied; defaults RECOMMENDED. Anchorer-defined codes MAY be added alongside.

Optional

FieldTypeNotes
revealed[i].salt_b64base64 stringMode-dependent. REQUIRED for salted carriers — the salted dotted profiles (e.g. deprecated satsignal.csv.row.v1) and the sealed native profiles (csv-row-v1 / text-line-v1 / json-keypath-v1, algo == "merkle-hmac-sha256", salt_version == "salt_v1"; the per-leaf HKDF salt, never the master salt). ABSENT for the standard native profiles (csv-row-v1 / text-line-v1 / json-keypath-v1, algo == "sha256", unsalted). The structural schema treats it as optional; the verifier enforces presence per the carrier chunk_merkle.algo (a salted or sealed carrier with salt_b64 missing fails closed; a standard carrier with a stray salt_b64 is ignored by the bare-sha256 leaf rule).
disclosure.presentationobjectRequired only if a redacted artifact ships with the disclosure (alongside the .mbnt — the bundle itself does not embed it). If the disclosure ships without a rendered view (e.g. raw-API consumer planning to render server-side or downstream), presentation MAY be omitted.
disclosure.presentation.formatenumRequired if presentation present.
disclosure.presentation.view_sha256hexRequired if presentation present.
disclosure.presentation.redaction_markerstringRequired if presentation present. Defaults to "[REDACTED]" only as an anchorer convention; the manifest still emits the literal value used.
disclosure.presentation.structure_disclosureenumRequired if presentation present.

Unknown sub-keys inside disclosure follow the bundle's standing tolerance rule (bundle-v1.md §9.2): a verifier MUST skip and continue on unknown keys; it MUST NOT fail closed on their presence. (Unknown version literals, by contrast, do fail closed — see §8.)

6. JCS canonicalization

The disclosure block participates in manifest.json canonicalization under the existing bundle rules: UTF-8 NFC, object keys sorted by code point, separators=(",",":"), integers as bare decimals, floats forbidden. The manifest's sha256 (where the bundle layout exposes one) covers the entire manifest including the disclosure block — there is no separate "disclosure hash" field; the disclosure's integrity rides on the manifest's integrity.

presentation.view_sha256 is a separate commitment. It covers the rendered artifact's bytes (the HTML / TXT / CSV / JSON file delivered alongside the .mbnt whose content is the redacted view), not the manifest bytes. A verifier checks the two hashes independently: the manifest binds the disclosure block to the proof; view_sha256 binds the rendered artifact to the disclosure block.

Leaf values (and salt_b64 when present, for salted carriers) are committed inside the manifest's JCS bytes, so any change to a revealed[] entry changes the manifest hash. A verifier that has access to the manifest bytes can detect any after-the-fact edit to the disclosure block by re-running JCS over the manifest.

7. Linked-anchor verification contract

A verifier processing manifest.disclosure MUST run these checks in order. Any failure terminates the disclosure check closed with the indicated code.

  1. Pin the version. Read disclosure.version. If it is not exactly "satsignal.disclosure.v1", fail closed with unsupported_disclosure_version.
  2. Bind to the original anchor. Run the binding-chain procedure in §4. All five checks there must pass before continuing. (Step §4.1 locates linked_anchor/canonical.json; §4.2 binds its sha256 to the on-chain document_hash at linked_anchor.txid; §4.3 binds the carrier's chunk_merkle.root to disclosure.linked_anchor.root; §4.4 binds its chunk_merkle.scheme to subject_profile — for a native CSV anchor the literal "csv-row-v1"; §4.5 handles the chunk_merkle.algo, accepting for csv-row-v1 either algo == "sha256" (standard, unsalted) or algo == "merkle-hmac-sha256" with salt_version == "salt_v1" (sealed, per-leaf HKDF salt). The sealed branch is the additive the additive lift — no version-literal bump; any other algo fails closed with unsupported_linked_algo.)
  3. Pin the profile. Read linked_anchor.subject_profile. If the verifier does not implement that profile literal, fail closed with unsupported_profile. Silent skip is not a legal behavior — silently skipping would let a future bad-profile leaf appear "verified."
  4. Per revealed leaf, recompute. For each revealed[i], apply the profile's leaf-hash rule selected by the carrier chunk_merkle.algo to recompute the leaf hash:
    • Salted dotted profiles (e.g. the deprecated satsignal.csv.row.v1, the salted text / json profiles): the profile's preimage over (profile, leaf_id, value, salt_b64) (the exact byte layout pinned per profile; the verifier MUST find salt_b64 present and fail closed if it is missing).
    • Standard native profile (csv-row-v1 / text-line-v1 / json-keypath-v1, algo == "sha256"): the bare sha256(utf8(value)) — no profile / leaf_id / salt framing, and no salt_b64 is required or read. The value is the canonical data row (csv-row-v1), canonical line (text-line-v1), or canonical top-level-key entry "key":jcs(value) (json-keypath-v1). See profiles/csv-row-v1.md §4 / profiles/text-line-v1.md §4 / profiles/json-keypath-v1.md §4.
    • Sealed native profile (csv-row-v1 / text-line-v1 / json-keypath-v1, algo == "merkle-hmac-sha256", salt_version == "salt_v1"): the keyed HMAC-SHA256(base64decode(salt_b64), utf8(value)), where salt_b64 is the per-leaf HKDF salt (never the master salt). salt_b64 is REQUIRED for sealed; if it is missing the verifier fails closed (sealed_leaf_missing_salt, the recompute cannot proceed). See profiles/csv-row-v1.md §5b / profiles/text-line-v1.md §5b / profiles/json-keypath-v1.md §5b.

Compare the recomputed hash against revealed[i].leaf_hash. Mismatch → fail closed (leaf_hash_mismatch). - Walk proof_path from the recomputed leaf hash. At each step, decode the 64-character lowercase hex digests of the sibling and the current frontier to their raw 32-byte values. Concatenate the raw bytes — (sibling_bytes || frontier_bytes) if the entry's side == "L", otherwise (frontier_bytes || sibling_bytes) — to form a 64-byte buffer, then SHA-256 that 64-byte buffer; that becomes the new frontier (re-encoded to 64-char lowercase hex for comparison). Verifiers MUST NOT concatenate the ASCII hex strings. If proof_path is the empty array [], the recomputed leaf_hash is itself the frontier (single-leaf tree case, §3.4 invariant 2). After the final step, the frontier MUST equal linked_anchor.root. Mismatch → fail closed (merkle_path_mismatch). - Confirm revealed[i].profile == linked_anchor.subject_profile. Mismatch → fail closed (profile_mismatch).

  1. Verify the rendered view, if present. If disclosure.presentation is present, obtain the rendered artifact — the redacted copy supplied ALONGSIDE the .mbnt (the bundle itself carries only manifest.json + linked_anchor/canonical.json; it does not embed the rendered view) — sha256 its raw bytes, and compare against presentation.view_sha256. Mismatch → fail closed (view_hash_mismatch). A verifier that is GIVEN the redacted copy MUST run this check; it may be skipped only when no rendered artifact is available, and a verifier that skips it MUST NOT report the presentation as verified.

All five checks must pass for the disclosure to be considered verified. Partial verification (e.g. "3 of 5 leaves passed") MUST NOT be reported as a success state; either the whole disclosure verifies, or the whole disclosure fails.

8. Verifier rendering behavior

A bundle whose manifest.disclosure is present is rendered as a partial-disclosure view, not as a regular proof. The verifier MUST:

9. Auto-anchor of the disclosure package

Optionally, the disclosure bundle itself can be anchored. When the anchorer chooses to do so, the disclosure .mbnt is submitted through the standard anchor path: POST /api/v1/anchors. The server treats the hash as opaque — it sees only sha256(canonical disclosure-bundle contents), the same as for any other standard anchor. There is no disclosure-specific endpoint, mode, or persisted column.

The Disclosure Builder (client-side) defaults this option on, so an anchorer publishing a disclosure typically gets a second on-chain commit binding the disclosure's exact bytes to a time T2 later than the original's T1. Raw-API consumers default it off — they may treat the disclosure as a transient handoff and never anchor it.

Default privacy posture: linked_anchor.root and linked_anchor.txid live in the disclosure bundle's manifest only, NOT in the canonical doc that goes on-chain at T2. The T2 commit therefore does not publicly expose the link back to the original anchor; only parties who hold the disclosure .mbnt see the link. This preserves the property that anchoring a disclosure leaks no more about the original than the disclosure itself does.

A public-link disclosure mode — where the link is written into a published sidecar field so a third party can resolve the original anchor from on-chain data alone — is out of scope for v1. It is gated behind a future decision record; an anchorer who wants that property today must publish the disclosure .mbnt themselves.

10. receipt_kind API surface

Spelling note. The field is emitted as proof_kindreceipt_kind is its pre-flip spelling, still accepted inbound but no longer emitted. The heading keeps the original name as a stable cross-reference target; the contract below is unchanged apart from the spelling.

A disclosure bundle's proof is read back through the existing endpoints — GET /api/v1/proofs/<id> and the legacy alias GET /api/v1/receipts/<id> — with one additive field on the response:

{
  "proof_id":   "…",
  "mode":       "standard",
  "proof_kind": "disclosure",
  …
}

The field's contract:

The field began as the deferred receipt_kind field (now spelled proof_kind): the additive-API-surface choice is deliberate and audits MUST NOT propose overloading mode instead.

11. Profile registry pointer

A profile pins the segmentation, canonicalization, normalization, salting strategy, and leaf_id construction rules under which the original document was hashed. Salting strategy is per-profile — it is not a global property of satsignal.disclosure.v1. The registered profile literals:

This is the primary CSV disclosure source — disclosures bind to the chunk_merkle the anchor already committed (scheme == "csv-row-v1"), with no re-anchor and no new scheme. Spec at profiles/csv-row-v1.md (§§2–8 standard, §5b sealed).

Disclosures bind to the chunk_merkle the text anchor already committed (scheme == "text-line-v1"), no re-anchor and no new scheme. Spec at profiles/text-line-v1.md (§§2–4 standard, §5b sealed).

The redacted copy ships in one of TWO owner-chosen render modes (default drop: revealed-keys-only canonical JCS object, positions hidden; mask: all keys sorted with withheld → "key":"[REDACTED]", positions preserved) — presentation-only, binding the revealed keys to the root. Disclosures bind to the chunk_merkle the JSON anchor already committed (scheme == "json-keypath-v1"), no re-anchor and no new scheme. Spec at profiles/json-keypath-v1.md (§§2–4 standard, §5b sealed, §7 render modes).

Profile literals are forever-contracts. Once any client has anchored under a literal, its rules are fixed for that literal forever. A bug in those rules cannot be patched in place; the only remedy is a new vN+1 profile that compatible verifiers must support in parallel. This is why each profile spec ships with its fixture set in the same PR and why a profile spec without test vectors is not mergeable.

This master spec does NOT define per-profile rules. The leaf-hash preimage, the exact salting strategy, the segmentation rules, and the leaf_id construction all live in the per-profile docs above.

12. Out of scope for v1

The following are explicitly out of scope for v1:

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