Beersy
BRC-220

NotaryHash — Privacy-Preserving Signed-Hash Notarization with SPV-Verifiable Certificates

Proving something existed and was signed at a certain time normally means publishing the thing itself. This puts only a hash on the chain and issues a anyone can check, so the content stays private.

Gregory Wardchanged 18 Sept 20267 min read

Reference for an AI

Everything an assistant needs to answer questions about BRC-220 accurately, including what it depends on.

Summary

Why
Blockchain notarization services typically require trusting the issuer's database or a block explorer, and often see the private document itself, when neither should be necessary to prove a signed hash existed at a point in time.
What
BRC-220 (NotaryHash) is a transaction format and certificate for anchoring a signed hash on the BSV blockchain so anyone can verify it offline against alone, without the document or the issuing service.
How
A signer hashes and signs content locally, sends only the hash and signature to a service, which builds a canonical binary proof, it in an output, and issues a JSON certificate that a verifier checks by re-verifying the signature, recomputing the proof hash, and confirming a Simplified Payment…

What this lets you do

  • Anchor a signed hash on-chain without exposing the underlying document
  • Verify a certificate fully offline except for obtaining one block header
  • Batch many proofs under a single to save on-chain space
  • Use classical or post-quantum signature algorithms in the same format
  • Attach an to a certificate after issuance without invalidating it

Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.

payloadHashsignaturesignature

The specification

Abstract

This proposal defines a transaction format and a self-contained for anchoring a signed hash to the Bitcoin blockchain. A signer proves they signed a specific hash; the on-chain fixes that proof in time at the block in which it is mined. The notarizing service never receives the original document and never any client private key. Certificates are independently verifiable — offline for the signature and proof integrity, and against alone () for the anchor — with no dependency on the issuing service or any chain-indexing API.

Motivation

Many "blockchain notarization" services require trusting the issuer's database, or a block explorer, to attest that a record exists on-chain. This proposal removes that trust and standardises an interoperable, privacy-preserving format so that any party can produce and verify these proofs:

  1. Privacy. Only SHA-256(content) and a signature over it leave the client; the document itself is never disclosed.
  2. Determinism. The integrity root is a fixed length-prefixed binary encoding (never JSON.stringify, which is not stable across implementations), so any implementation in any language reproduces identical bytes.
  3. Post-quantum readiness. Classical (ECDSA) and post-quantum (ML-DSA, SLH-DSA) signatures are first-class, so proofs intended to last remain verifiable as classical schemes weaken.
  4. Trustless verification. A certificate carries an , so a verifier confirms the anchor using only Bitcoin block headers (BRC-9, BRC-10/BRC-11).

Specification

Roles

  • Signer hashes content locally, signs the hash locally, and submits {algorithm, payloadHash, publicKey, signature}.
  • Service verifies the signature, builds , anchors them in an OP_RETURN, and returns a certificate.
  • Verifier re-checks the signature and proof bytes offline and confirms the anchor.

Algorithms

familyidshash
ECDSAECDSA-secp256k1SHA-256
ML-DSA (FIPS 204)ML-DSA-44, ML-DSA-65, ML-DSA-87SHA-256
SLH-DSA (FIPS 205)SLH-DSA-{SHA2,SHAKE}-{128,192,256}{s,f} (12 parameter sets)SHA-256

The signer signs the 32-byte payloadHash directly; post-quantum schemes apply their own internal hashing. For ECDSA-secp256k1, payloadHash is the message digest H of SEC 1 §4.1.3: it is read as a big-endian integer and is not hashed again. Some ECDSA libraries hash their input by default, and some Bitcoin libraries can read the digest as little-endian; either produces a signature over a different value, which conformant verifiers reject.

Canonical proof bytes (integrity root)

proofHash = SHA-256(canonicalBytes), where:

lp("NotaryHash/1.0") || u8(version=1) ||
lp(algorithm) || lp(hashAlgorithm) ||
lp(payloadHash) || lp(publicKey) || lp(signature) ||
u64be(createdAtUnix)

lp(x) = u32be(len(x)) || x; u8/u64be are unsigned big-endian integers. All multi-byte fields are length-prefixed, so field boundaries are unambiguous regardless of content. This binary encoding — never JSON — is what makes the proof reproducible across implementations.

On-chain record (OP_FALSE OP_RETURN)

A safe data output whose pushes are discriminated by the mode/kind byte at push index 2:

  • full (mode = 0): "NOTARYHASH" | u8(1) | u8(0) | algorithm | hashAlgorithm | payloadHash | proofHash | publicKey | signature.

  • hybrid (mode = 1): as full, but the final two pushes are SHA-256(publicKey) and SHA-256(signature); the full blobs live in the certificate (keeps large post-quantum records small on-chain).

  • batch (kind = 2): "NOTARYHASH" | u8(1) | u8(2) | merkleRoot(32) | u32be(leafCount). One transaction anchors many proofs under an RFC 6962 , domain-separated (leaf = SHA256(0x00 ‖ d), node = SHA256(0x01 ‖ l ‖ r), split at the largest power of two < n, last never duplicated).

    The leaf datum d for each proof is its 32-byte proofHash, so a leaf is SHA256(0x00 ‖ proofHash) — that is, SHA256(0x00 ‖ SHA256(canonicalBytes)), not SHA256(0x00 ‖ canonicalBytes). Both are sound, but they produce different roots, so the choice is stated rather than left to RFC 6962's generic d. Leaves are in the order the batch was assembled, and leafIndex in the certificate's merkle object is that position, counted from 0.

Certificate

A self-contained JSON object, canonicalised via RFC 8785 (JCS) for hashing and transport, carrying the twelve required fields below. Hex is written lowercase, without a 0x prefix; a reader MAY accept upper case and the prefix. Numbers are JSON integers. Verifiers ignore members they do not recognise, which is how the SPV envelope is added to a certificate already issued.

fieldvalue
protocolthe string "NotaryHash"
versionthe string "1.0": the certificate format version. It corresponds to u8(version=1) in the canonical proof bytes and the on-chain record, and to the domain separator "NotaryHash/1.0", but is written as a string. A verifier MUST reject a certificate whose version it does not implement.
mode"full" or "hybrid": how the proof is recorded on chain, corresponding to the on-chain mode byte 0 or 1. Batching is marked by anchor.type, not by mode.
algorithman identifier from §Algorithms, e.g. "ECDSA-secp256k1"
hashAlgorithm"SHA-256"
payloadHashthe 32-byte payload hash, hex
publicKeythe full public key in every mode (hybrid puts only its SHA-256 on chain), written per encoding
signaturethe full signature, as the signer produced it, in every mode, written per encoding
encodinghow publicKey and signature are written: "hex", or "base64" (RFC 4648 §4: standard alphabet, padded). It applies to those two fields only, and says nothing about the format of the bytes themselves. A reader MUST reject a value that is not valid in its encoding rather than decode what remains of it — for base64, a character outside the RFC 4648 §4 and §5 alphabets, padding anywhere but the end, or a length no byte string encodes. A reader MAY accept the §5 (URL-safe) alphabet and missing padding.
proofHashthe 32-byte SHA-256(canonicalBytes), hex
createdAtcreatedAtUnix as an ISO 8601 UTC timestamp with milliseconds, e.g. "2026-01-01T00:00:00.000Z". The milliseconds are always 000, because only whole seconds enter the canonical bytes; a verifier recovers createdAtUnix as the whole seconds the timestamp denotes. Advisory only — see §Verification.
anchorthe object below

For ECDSA-secp256k1, publicKey is 33 bytes (compressed) or 65 (uncompressed), and signature is 64 bytes (r ‖ s, each a 32-byte big-endian integer) or DER. A verifier reads a 64-byte signature as r ‖ s, and any other signature as DER, which must parse as DER. DER can itself be 64 bytes long — only when r and s are unusually short — and is then read as r ‖ s and does not verify; a signer holding such a signature sends it as r ‖ s instead. S is not normalised: the certificate commits, through proofHash, to the exact bytes the signer produced, so the malleated form of a signature is a different certificate rather than a forgery of this one.

anchor locates the on-chain record:

membervalue
type"direct" if the record carries this proof (mode 0 or 1); "batch" if it carries a Merkle root (kind = 2)
networkthe chain the anchoring transaction is on: "bsv-mainnet" for BSV mainnet, "bsv-testnet" for BSV testnet. Other values are not interoperable. The field is descriptive: which chain the anchor is on is established by the block header the verifier obtains, not by this value.
txidthe anchoring transaction's id, hex, in display order: reverse(SHA256(SHA256(rawTx)))
voutthe index of the OP_RETURN output within that transaction
blockHeightthe height of the block that mined the transaction, or null until it is mined
blockTimethat block's timestamp in Unix seconds, or null until it is mined

A batched certificate (anchor.type "batch") additionally carries merkle, the proof that this proof is one of the batch's leaves:

membervalue
rootthe 32-byte batch root, hex; equal to merkleRoot in the on-chain batch record
leafIndexthis proof's position in the batch, from 0
leafCountthe number of proofs in the batch; equal to leafCount in the on-chain batch record
paththe audit path, leaf → root, as an array of { "hash": <32-byte hex>, "side": "left" or "right" }. side is the sibling's position relative to the running hash. Starting from SHA256(0x00 ‖ proofHash), a "left" sibling folds as SHA256(0x01 ‖ hash ‖ running) and a "right" one as SHA256(0x01 ‖ running ‖ hash). The result must equal root.

In a batched certificate mode is not checked against the chain: the batch record carries neither the proof nor a mode byte.

A direct-anchored ECDSA certificate, before its SPV envelope is attached:

{
  "protocol": "NotaryHash",
  "version": "1.0",
  "mode": "full",
  "algorithm": "ECDSA-secp256k1",
  "hashAlgorithm": "SHA-256",
  "payloadHash": "97ef50e782e55cfbfbfb0c6199b96837836b7f7b0fcae76f79a65e2466dc596b",
  "publicKey": "02375ac16df62a74475844721d6a180927f29314c3455eaa699f7dcf5237c36e52",
  "signature": "e65171edb82a702a8390fb90f005ba3d4e174ab945ab0bcaf5f1f8d18c3547426ca419ea252b142e863d2eeebdfc787a624bed5a06d23a671959b07b5cb12ba3",
  "encoding": "hex",
  "proofHash": "1b34fbeb640e63b366751a279aa749e4f279cfb7e1add1a96150d7fd7c2ff05d",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "anchor": {
    "type": "direct",
    "network": "bsv-mainnet",
    "txid": "460d19b875036e41d6dea392bfcb84508120e7b573080c45869eedbae39dec6a",
    "vout": 0,
    "blockHeight": 900000,
    "blockTime": 1767225600
  }
}

The merkle member of the last certificate in a five-proof batch:

{
  "root": "abb53eb3b2e3530d51685c6813bb85c85892d2f214c2dc504029d071434ac074",
  "leafIndex": 4,
  "leafCount": 5,
  "path": [
    { "hash": "060ca4e4bbcb647ab0162bb027cd8f08c1577aaa7069166dd629a3df7e57befd", "side": "left" }
  ]
}

The anchoring transactions and blocks in these examples are test fixtures, not mainnet data. Everything else in them verifies: the signature, proofHash, the on-chain record in the transaction, and the batch inclusion.

SPV envelope (additive)

Attached once the anchoring transaction is mined:

"spv": {
  "rawTx": "<hex>",
  "blockHash": "<hex>",
  "blockHeight": 0,
  "merkleProof": { "index": 0, "nodes": ["<hex>", "*", "..."] },
  "format": "TSC"
}

merkleProof is a Merkle inclusion proof of txid under the block's Merkle root, expressed in the BRC-10/BRC-11 (TSC) model; a BRC-74 () or BRC-62 () encoding MAY be substituted by setting format accordingly. The SPV envelope is not part of the canonical proof bytes, so adding it never changes proofHash and never invalidates a previously issued certificate.

rawTx is hex, and blockHash is hex in display order, like txid. In the "TSC" format the entries of merkleProof.nodes are hex in display order too, and an entry of "*" means "duplicate the working hash", the TSC convention for a missing right sibling. The verifier checks that the header it obtained has hash spv.blockHash and height spv.blockHeight.

Verification

A certificate is valid if and only if all of the following hold:

  1. Signatureverify(algorithm, payloadHash, signature, publicKey) is true. (offline)
  2. Proof integrity — the recomputed proofHash equals certificate.proofHash. (offline)
  3. Anchor — one of:
    • SPV (preferred): txid = reverse(SHA256(SHA256(rawTx))) equals anchor.txid; the OP_RETURN read from rawTx matches the certificate fields; folding merkleProof from txid yields a root equal to the Merkle root of the block header for spv.blockHash at spv.blockHeight. The verifier trusts only a block header, obtained from any source it chooses (a synced header chain, or several independent sources cross-checked) — not a provider's word about the transaction (BRC-9).
    • Direct: read the OP_RETURN at anchor.txid from a chain provider (legacy certificates with no SPV envelope).

Steps 1–2 require no network. The proof-of-existence time is the block time of anchor.txid; createdAt is an advisory client field only.

What a certificate proves

A specific public key signed a specific hash, and that proof was anchored on-chain at/by the block timestamp. It does not establish who submitted it (any party holding a valid (hash, signature, publicKey) triple may re-anchor it; the attestation remains valid), nor anything about the document's contents (a verifier needs the document to recompute the hash).

Security considerations

  • The SPV trust model reduces to obtaining a correct block header. A single header source is a single point of trust; a multi-source quorum, or a proof-of-work-validated header chain, removes it. Implementations SHOULD make the header source explicit.
  • Provider-supplied data is self-checking: a raw transaction is accepted only if its double-SHA-256 equals the txid already held, so a provider cannot substitute different bytes.

Implementations

A reference implementation (service, client SDK, and a dependency-light standalone verifier) is available, including:

  • a standalone certificate verifier that depends only on the protocol's own modules (signature + proofHash + SPV anchor), never on the issuing service;
  • a confirmation poller that attaches the SPV envelope once the anchoring transaction is mined; and
  • published test vectors: a complete certificate-with-SPV-envelope golden vector, txidFromRawTx checked against the Bitcoin genesis transaction, and the Merkle fold checked against the real block-170 (two-transaction) proof.

Batch-mode test vector

Five proofs, so the tree splits 4/1 and exercises both the largest-power-of-two split and the never-duplicate rule. For i from 0 to 4, written as one ASCII decimal digit: private key SHA-256("BRC-220/batch-vector/key/" + i) read as a big-endian integer, payloadHash SHA-256("BRC-220/batch-vector/payload/" + i), createdAtUnix 1767225600 + 86400 * i (seconds), algorithm ECDSA-secp256k1, hashAlgorithm SHA-256, compressed public key, 64-byte r ‖ s signature with an RFC 6979 , normalised to low-S. (Two of the five signatures differ without that normalisation, so it is needed to reproduce the values below.)

leafIndexproofHash
083f3f73ebc5146b96cce1ee0b6ed4fd251a933a5332fbe5e5ab80da7920fe2c9
129fca10808a3653efbe38abd5f0b926aa09f13bff7847475139a422869729e77
23c44828bea3ca01dfcd5305a839ce02cc1e855f5a7c7028bb4931d42dea3aa2b
30692e22da9b68634e5384bb012324c140c676d2301e078a67f17e335661a9565
41a9c6bc1a6cf8319344ca8c30ca9e9eef8ab3df732748f6be2c824bb84ec0ca9
  • merkleRoot (d = proofHash): abb53eb3b2e3530d51685c6813bb85c85892d2f214c2dc504029d071434ac074
  • audit path for leaf 4 (one node, left sibling): 060ca4e4bbcb647ab0162bb027cd8f08c1577aaa7069166dd629a3df7e57befd
  • root an implementation computes if it uses d = canonicalBytes instead: 4a59ade74941e4d6ca4b7c9f610e0a01a0c3f24cfa94efe816e53ef85f99e324

The format has been demonstrated end-to-end on BSV mainnet: a certificate verifies fully offline against independently obtained block headers, with no trust in the issuing service.

References

Was this helpful?

Search Beersy

Search standards by number, title, author or topic