What's on chain — a worked MBNT anchor

A Satsignal anchor is a single BSV transaction with one OP_FALSE OP_RETURN output that carries an MBNT payload. Everything else about a Satsignal receipt — the canonical document, attached files, manifest leaves, sealed reveals — lives off chain. The chain sees a 20-byte hash and a few bytes of indexable metadata.

This page walks one real preview-tier anchor end to end so you can reproduce the parse yourself with no Satsignal account, no installed software, and nothing more than a hex viewer.


1. The transaction

Pick any preview-tier anchor. Below is one we use as a worked example:

Open either explorer link and you'll see one P2PKH input, one P2PKH change output, and one OP_RETURN output. The OP_RETURN is what carries the Satsignal commitment.


2. The OP_RETURN script

The full scriptPubKey of the OP_RETURN output is 36 bytes:

6a 22  4d 42 4e 54  01 01  00 06
ae 5c 19 9f 31 a0 63 6f 0b ee d0 f1 c8 3e 04 b0 8e 1a c9 ae
05 04 d5 b0 b0 c6

Two-byte script wrapper, then 34 bytes of payload:

bytesrolemeaning
6aOP_RETURNoutput is unspendable, used for data
22push opcodepush the next 34 (0x22) bytes literally
4d 42 4e 54magicASCII "MBNT" — Satsignal's protocol prefix
01versionMBNT v1
01subtypegeneric (see /spec-mbnt §4)
00 06tlv_len6 bytes of TLV section follow the hash
ae5c…c9aedoc_hashfirst 20 bytes of sha256(canonical_doc)
05 04 d5 b0 b0 c6TLVtag=0x05 issuer_id, len=4, value 4 B

That's the entire on-chain commitment. A non-Satsignal verifier reading this transaction has everything they need to walk from txid to a 20-byte document fingerprint.


3. What the doc_hash commits to

The 20 bytes ae5c199f31a0636f0beed0f1c83e04b08e1ac9ae are sha256(canonical_doc_bytes)[:20], where canonical_doc_bytes is the JCS-canonical UTF-8 encoding of the receipt's canonical-doc JSON. Canonicalization rules: NFC-normalize all strings, sort all dict keys, no whitespace, no floats, no NaN/Infinity. The same recipe merkle_row.py and commit_reveal.py use.

A third party who has been handed (txid, canonical.json) can verify the binding with stdlib JSON + SHA-256 in about ten lines:

import json, hashlib

def canonicalize(obj):
    # JCS: sort keys, no whitespace, ensure_ascii=False
    return json.dumps(obj, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False).encode("utf-8")

doc = json.load(open("canonical.json"))
expected = hashlib.sha256(canonicalize(doc)).digest()[:20].hex()
assert expected == "ae5c199f31a0636f0beed0f1c83e04b08e1ac9ae"

If expected == doc_hash, the canonical document was committed in this transaction at the chain's confirmation time. If they differ, the document has been modified or the txid points to a different proof — full stop, no partial credit.


4. What the issuer_id TLV commits to

The trailing 6 bytes 05 04 d5 b0 b0 c6 are an issuer_id TLV:

issuer_id = sha256(operator_did_utf8)[:4]

For d5b0b0c6 the matching DID is did:web:satsignal.cloud — Satsignal's hosted preview tier. Every preview-tier anchor carries the same 4 bytes. This is the protocol's discoverability handle: chain indexers can enumerate Satsignal-issued anchors without needing any side-channel knowledge.

It also means every preview-tier anchor is publicly chain-tagged with this fingerprint. For workloads where unlinkability matters (sealed-bid auctions among non-trivial counterparties, whistleblower-class users) the operator should suppress the TLV per-receipt (publish.issuer = False) so the payload shrinks to the strict 28-byte header. See /spec-mbnt §11 for the full property and how to opt out.


5. Confirmation depth

A transaction in the mempool is a commitment to the broadcast network. A transaction in a block is a commitment to chain history. Verifiers SHOULD display confirmation count next to the txid and apply a minimum-depth gate appropriate to the use case:

use caseminimum
audit-trail / evidence dispositioning≥ 1
sealed-bid auction reveal-after / fairness≥ 1
settlement / counterparty-risk gating≥ 6

Anchors typically land within 3–8 confirmations (~30–80 minutes on BSV mainnet). Both WhatsOnChain and Bitails return a confirmations field on the tx-JSON endpoint (/v1/bsv/main/tx/hash/<txid> for WoC).

The Satsignal /verify page reads this for you and shows the count next to the on-chain match pill.


6. What is NOT on chain

A 36-byte script can carry a 20-byte hash and 4 bytes of discoverability metadata. It cannot — and is deliberately not designed to — carry:

All of these live in the off-chain bundle. The chain commits to them via the 20-byte hash; the bundle holds the bytes. A verifier who has only the txid can confirm the anchor exists and read (version, subtype, doc_hash, issuer_id). To verify any specific claim about the document, they need the bundle.

This split is the entire architectural wedge: large-by-bytes content stays off chain, large-by-truth commitment goes on chain.


7. Reproducing the parse from scratch

Three steps, no Satsignal API:

  1. Fetch the raw transaction hex from a public BSV node:

`` curl -s https://api.whatsonchain.com/v1/bsv/main/tx/7e0603...ebbc/hex ``

  1. Walk inputs and outputs (varint-prefixed, see BSV tx format). For each output's scriptPubKey, look for 00 6a (OP_FALSE OP_RETURN) followed by a single push whose first 4 bytes are MBNT.
  2. Parse the payload per /spec-mbnt §2 — magic, version, subtype, tlv_len, 20-byte doc_hash, 0–192 bytes of TLVs.

A 25-line reference implementation lives in opreturn.find_mbnt_payload_in_raw_tx in the source repo. JavaScript port lives in parseMbnt() on the /verify page.


8. Going further

The chain anchoring is the entire product wedge. It is also the part that has nothing proprietary about it — anyone with a hex viewer and ten lines of Python can verify a Satsignal anchor.

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