MBNT on-chain wire format

The MBNT payload is the bytes that appear inside the OP_FALSE OP_RETURN output of a Satsignal anchor transaction. Everything else about a Satsignal receipt (canonical doc, manifest, miner acceptance, merkle proofs) is off-chain — but the chain commitment lives here, and a non-Satsignal verifier reading a transaction needs to parse this payload to walk from txid to a 20-byte document hash.

This page is the standalone wire-format spec. It supersedes the description embedded in parseMbnt() in verifier.html. The reference implementations are:

Both produce/accept byte-identical payloads.


1. Where the payload lives

A Satsignal anchor is a single-output BSV transaction whose first OP_FALSE OP_RETURN output carries one MBNT payload. The script is

OP_FALSE OP_RETURN <push N> <payload bytes>

<push N> is either a single-byte push opcode (0x01..0x4b) or OP_PUSHDATA1 (0x4c) followed by a 1-byte length, depending on total payload size. Total payload is between 28 and 220 bytes (the upper bound is BSV's data-carrier relay norm — Satsignal does not emit larger payloads).

To extract the payload from a raw tx hex: walk inputs/outputs; for each output's scriptPubKey, check for the 00 6a prefix and a single push; the pushed bytes whose first 4 bytes are MBNT is the payload.

A reference implementation in 25 lines lives in opreturn.find_mbnt_payload_in_raw_tx. Other-language ports tend to be the same shape.


2. Byte layout

offset  size  field        encoding
------  ----  -----------  ------------------------------------
   0     4    magic        ASCII "MBNT" — 0x4D 0x42 0x4E 0x54
   4     1    version      currently 0x01 (sole shipped value)
   5     1    subtype      see §4 (subtype registry)
   6     2    tlv_len      uint16, big-endian, bytes of TLV section
   8    20    doc_hash     first 20 bytes of sha256(canonical_doc)
  28    N     tlvs         tag-length-value entries, see §5
                           N = tlv_len, 0..192
------  ----
total  28+N   payload      28..220 bytes

A minimal MBNT payload (no TLVs) is exactly 28 bytes:

4D 42 4E 54   01 01   00 00   <20 bytes of doc_hash>
└ "MBNT" ┘   v  st   tlv_len  └─── doc_hash ─────┘

A payload with two TLVs (currency=USD, timestamp_unix=epoch s) is 36 bytes:

4D 42 4E 54  01 01  00 0E   <20 bytes>
01 03 55 53 44                            // tag=01 (currency), len=3, "USD"
06 08 00 00 00 00 68 31 7C 80             // tag=06 (timestamp_unix), len=8, BE u64

3. Magic and version


4. Subtype registry

The subtype byte selects the schema applied to the off-chain canonical document the doc_hash commits to. Codes are 1 byte, never reused:

codenamemeaning
0x01genericunspecified document; the canonical doc itself names its schema in a subject.kind field
0x02wirea financial-wire receipt schema
0x03doc_signa document-signing receipt schema
0x04eventan event-log receipt schema

generic (0x01) is what every Satsignal anchor produced by the public API ships today. The other three codes are reserved for private deployments running their own canonical-doc schemas.

A verifier that doesn't recognize a subtype byte SHOULD still report the on-chain commitment (doc_hash, txid, version) but MUST NOT claim to have validated the canonical document — it doesn't know which schema applies.


5. doc_hash

doc_hash is exactly 20 bytes, computed as

doc_hash = sha256(canonical_doc_bytes)[:20]

canonical_doc_bytes is the JCS-canonical UTF-8 encoding of the canonical-doc JSON object. The canonicalization algorithm (NFC normalize all strings, sort all dict keys, emit minimal JSON, no whitespace, no NaN/Infinity, no floats) is the same one used in merkle_row.py §1.1 and the commit_reveal.py helper.

The canonical-doc shape itself depends on the receipt category and mode. For each shape, the spec is:

In every case the recipe is the same: re-canonicalize the same JSON, sha256 it, take the first 20 bytes, compare to the on-chain doc_hash.


6. TLV section

The 0–192 bytes after the header carry zero or more tag-length-value entries:

[tag (1 B)] [length (1 B)] [value (length B)]

Verifiers MUST:

The total TLV section size is capped at 192 bytes so that 28 + tlv_len ≤ 220 (BSV relay norm).


7. TLV tag registry

Public API anchors today emit one TLV by default — issuer_id (tag 0x05) — committing the 4-byte hash of the operator's DID. See §11 for what that means as a public chain-property and how to opt out. The other tags below are reserved for private deployments that want additional indexable on-chain metadata:

tagnamelengthvalue encoding
0x01currency3ASCII (e.g. b"USD")
0x02amount_bucket1floor(log10(amount_in_minor_units)) (0..9)
0x03reference_hash8sha256(reference_id_utf8)[:8]
0x04counterparty_hash16sha256(counterparty_id_utf8)[:16]
0x05issuer_id4sha256(issuer_pubkey)[:4]
0x06timestamp_unix8unsigned 64-bit big-endian seconds since epoch
0x07subdoc_hash20secondary 20-byte document commitment

Values are public on chain — never put PII or secrets in TLVs. Use the off-chain canonical doc and let doc_hash commit to them.

New tags are appended to this registry as private deployments need them; existing codes are never repurposed.


8. End-to-end verification recipe

A non-helper-language third party who has only (txid, canonical_doc_bytes) can verify the binding in any language with stdlib JSON + SHA-256:

  1. Fetch the raw transaction by txid from any public BSV node (e.g. WhatsOnChain or Bitails).
  2. Walk the outputs; find the first OP_FALSE OP_RETURN <push> whose pushed bytes start with the magic MBNT.
  3. Parse the payload per §2: confirm magic, confirm version 0x01, read subtype, read tlv_len, slice doc_hash = payload[8:28].
  4. Re-canonicalize the supplied canonical doc (NFC, sort keys, no whitespace, no floats) and compute expected = sha256(...)[:20].
  5. If doc_hash == expected (constant-time compare, byte-equal), the document was committed in this transaction and the chain timestamp is its anchor time. If they differ, the document does not match this anchor — full stop.

That is the entire chain-side check. No Satsignal API is involved. For category-specific claims (the document's payload, manifest leaves, sealed reveals) the auditor then follows the spec for that category to walk the canonical doc.

For a worked example walking a real preview-tier txid byte by byte, see /whats-on-chain.


9. Operational notes


10. Confirmation depth for verifiers

The chain commitment is observable from the moment a transaction is broadcast, but a transaction is not final until it is mined into a block and that block is buried under further work. Verifiers SHOULD display confirmation count prominently next to the txid and apply a minimum-depth gate appropriate to the use case:

use caserecommended minimum
audit-trail / evidence dispositioning≥ 1
sealed-bid auction reveal-after / fairness≥ 1
settlement / counterparty-risk gating≥ 6
0 confirmations (mempool only)display "unconfirmed" — never claim "anchored"

A 0-confirmation receipt is a valid commitment to the broadcast network but not yet to chain history; a verifier that treats it as anchored is conflating two distinct properties. Anchors typically land within 3–8 confirmations (~30–80 minutes on BSV mainnet).

This is a verifier-side rule, not a protocol-byte change — the MBNT payload itself carries no confirmation field. Verifiers fetch confirmation count from the explorer they use to retrieve the raw tx (e.g. WhatsOnChain /v1/bsv/main/tx/hash/{txid} returns a confirmations field).


11. What an anchor publicly commits to

Beyond doc_hash, a Satsignal anchor publicly commits the operator's DID fingerprint via the issuer_id TLV (§7, tag 0x05):

issuer_id = sha256(doc["issuer"].encode("utf-8"))[:4]   # 4 bytes

For Satsignal's hosted preview tier this resolves to a constant — d5b0b0c6 == sha256("did:web:satsignal.cloud")[:4] — so every preview-tier anchor is publicly chain-tagged with these same 4 bytes.

Consequence. A chain observer who knows the operator's DID can enumerate every anchor that operator has issued. A multi-tenant deployment that gives each customer their own DID makes each customer's anchors enumerable by anyone watching the chain, even though the underlying documents stay sealed by the 20-byte hash.

For most use cases this is the intended property — discoverability of "Satsignal-issued anchors" by ecosystem indexers is half the reason a public protocol prefix exists. It is not appropriate for sealed-bid auctions among non-trivial counterparties or whistleblower-class users where "the bid was placed via Satsignal customer X" leaks before the reveal.

Opt-out. A caller can suppress the issuer_id TLV per-receipt by setting publish.issuer = False on the pipeline call (see pipeline.prepare_receipt in the source repo). The result is a strict 28-byte payload — no operator fingerprint on chain. Operators running deployments where unlinkability matters should set this as the default and treat opt-in as the exceptional path.


12. Versioning

MBNT v1 is stable. A future MBNT v2 would change the version byte to 0x02, may extend the header, and may introduce new subtype codes. This document specifies v1 only.

Verifiers MUST refuse unknown versions and unknown subtypes (with the note in §4 about reporting the on-chain commitment for unknown subtypes). Silently accepting a future version would defeat the spec-as-contract.

Source: docs/notary_spec/SPEC_mbnt.md. Email hello@satsignal.cloud for clarifications.