30817:nip-332-assumeutxo-attestations
NIP-332: assumeUTXO Attestations
draft optional
Canonical copy: https://nip-332.github.io/spec/ · source: https://github.com/nip-332/nip-332.github.io/blob/gh-pages/spec/nip-332.md
This NIP defines an event in which a Bitcoin node attests to the hash of the UTXO set at a block, as used by assumeUTXO snapshots, and the event in which a node disputes one.
Abstract
An assumeUTXO snapshot lets a node start from a serialized UTXO set instead of replaying every block. The snapshot's identity is the hash of that set at a given block. Today the only hashes a node trusts are the ones compiled into its own software, and the only record of who else checked them is scattered across review threads. This specification defines a signed, machine-readable statement by which any node that has computed the hash, or loaded the snapshot and validated the chain behind it, says so under its own key. The statement is free to make and permanent: a wrong attestation stays visible, so a key's history is its reputation.
Event Format
Kind
3332
A regular event (1000-9999 range). Relays keep every event. An attestation is never replaced: a correction is a new event that references the old one (see Corrections). The addressable range was deliberately not used, because it would let a key overwrite a wrong attestation and have relays forget it.
Tags
Tags repeat the subject for relay queries. The content is authoritative; a verifier MUST check that any of these tags present agree with the content and MUST reject the event if they do not.
| Tag | Required | Description |
|---|---|---|
v |
Recommended | The statement's version, repeated so relays index it and clients can filter #v for versions they read; the content decides if they differ, and a missing tag means "read the content" |
network |
Recommended | Network label: btc, tbtc3, tbtc4, signet, xbt, txbt4, … (a label, not an identity; see Identity) |
height |
Recommended | Block height of the snapshot, as a decimal string |
hash |
Recommended | Block hash of the snapshot's block |
txoutset_hash |
Recommended | The UTXO set hash attested |
method |
Recommended | recomputed or loaded |
implementation |
Optional | The node's BIP 14 user agent |
client |
Optional | What signed the event: name, URL, version |
t |
Optional | attestation, assumeutxo, and the network label |
e |
Corrections and disputes | The id of the attestation being corrected (marker correction) or disputed (marker disputes, kind 3331) |
Content
The content is a statement: a JSON object canonicalised with JCS (RFC 8785), so the signed bytes are the same whoever produces them. It has a version, a type, a subject (what is attested, identical for every attester of the same snapshot) and a claim (what this node did to know it).
{
"v": "0.0.1",
"@type": "Attestation",
"subject": {
"network": "txbt4",
"genesis": "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043",
"height": 150307,
"blockHash": "000000000017ec2251d81c8d2ca401c713e98e85196c7f660a4088a7ca57b1cc",
"txoutsetHash": "372bfcaeef1e93892acccda9e700b00bf90f45d73746d69959d33620a3df5518",
"hashType": "hash_serialized_3",
"coins": 14230182,
"chainTxCount": 14813091,
"fileSha256": "86118db3a692b7c64bd4d9aef51a401f09722b9e92acb2826e7e76ac964529cd"
},
"claim": {
"method": "recomputed",
"chainstate": "full",
"validated": true,
"implementation": "/Satoshi:29.4.2/Knots:20260508/Plumb:6/",
"build": "v29.4.2.knots20260508.plumb6",
"commit": "532e1eb168b743c5500d9fec0aad73ad6abfd0ac",
"binarySha256": "a454b330e6fefeac983856ca00a924e21aadf59de5794de4165392eb776416f6",
"computedAt": "2026-10-16T06:17:56Z",
"note": "dumptxoutset rollback=150307 on an unpruned node at 153068, 7 seconds"
}
}
v is the statement's version, 0.0.1 until the first attestation worth a name is signed and bumped on every change before that;
a verifier reads it first. @type is Attestation; the JSON-LD context defining it is published beside the statement, never inside
the signed bytes, which carry no hosts. There is no signer field: identity lives in the envelope, so the same statement can be signed
by a Nostr key or, in a sibling format, a GPG key.
Subject
| Field | Required | Description |
|---|---|---|
blockHash |
Yes | The hash of the block the set is taken at |
txoutsetHash |
Yes | The hash of the UTXO set at that block |
hashType |
Yes | The function behind txoutsetHash: hash_serialized_3 (Bitcoin Core 28.0 and later; what dumptxoutset writes and chainparams pin). Future values: muhash, later serializations |
height |
Recommended | The block's height |
network |
Recommended | A label for readers and queries |
genesis |
Recommended | The chain's genesis block hash, so the statement reads without a network list |
coins |
Optional | Number of unspent outputs in the set (coins_written from dumptxoutset, txouts from gettxoutsetinfo) |
chainTxCount |
Optional | Transactions in the chain up to and including the block (nchaintx; m_chain_tx_count in chainparams) |
fileSha256 |
Optional | SHA-256 of the snapshot file the node wrote: a possession claim, separate from the hash claim |
Claim
| Field | Required | Description |
|---|---|---|
method |
Yes | recomputed: this node built the set from blocks. loaded: this node started from the snapshot and background validation finished |
chainstate |
Yes | Where this node's own chainstate came from, from getchainstates: full, or snapshot:<height> |
validated |
Yes | Whether that chainstate is validated, from getchainstates |
implementation |
Optional | The node's BIP 14 user agent, read from the running node |
build |
Optional | The release tag the binary was built from |
commit |
Optional | The source commit |
binarySha256 |
Optional | SHA-256 of the node binary |
computedAt |
Optional | When the node computed the hash (not when the statement was signed), ISO 8601 UTC |
note |
Optional | Free text, at most 200 characters, no links |
Rules: loaded requires validated: true and a chainstate naming the snapshot. recomputed requires validated: true, and a
node whose chainstate came from a snapshot at or above the attested height has recomputed nothing and MUST NOT say recomputed.
An optional field, when present, MUST be true and is checked like a required one. implementation, build, commit and
binarySha256 are self-reported: readers show them as said, not as fact; only a reproducible build with signatures ties them to a binary.
Example Event
{
"kind": 3332,
"pubkey": "c77fba35e5cdf0f3e7c37655c6ccd19924c462f5ae9629cf2f9fa0950a59199d",
"created_at": 1792132900,
"tags": [
["v", "0.0.1"],
["network", "txbt4"],
["height", "150307"],
["hash", "000000000017ec2251d81c8d2ca401c713e98e85196c7f660a4088a7ca57b1cc"],
["txoutset_hash", "372bfcaeef1e93892acccda9e700b00bf90f45d73746d69959d33620a3df5518"],
["method", "recomputed"],
["implementation", "/Satoshi:29.4.2/Knots:20260508/Plumb:6/"],
["client", "assumeutxo sign", "https://play-grounds.github.io/assumeutxo/sign/", "0.0.1"],
["t", "attestation"],
["t", "assumeutxo"],
["t", "txbt4"]
],
"content": "{\"@type\":\"Attestation\",\"claim\":{\"chainstate\":\"full\",…},\"subject\":{\"blockHash\":\"000000000017ec22…\",…},\"v\":\"0.0.1\"}",
"id": "707332a2ca8390e147fc599bcdfe88dacf7a59b2c0e470a7aba1e15d9af441da",
"sig": "f17ab2c3b1e5ba74eeba6e778f7cba9974a8a80e9f3e9e8e5e855e15c301338a938bece0fb5453433116f33b5ced3c8b2cd04267bf05a015cba48b8b294f9ecf"
}
(A test event signed by a throwaway key; the content is shown shortened.)
Identity
A snapshot is identified by genesis, height and blockHash, and by nothing else. Two chains can share a genesis (a fork does), and
names and magic bytes are set by the client, so network is a label, not an identity. A verifier with a node of the named chain checks
that blockHash is on that chain's best chain at that height and rejects a mislabelled event. Below a fork point two chains are one
chain: an attestation of a block that lies on more than one chain holds for all of them.
Subscription
["REQ", "attestations", {
"kinds": [3332],
"#txoutset_hash": ["372bfcaeef1e93892acccda9e700b00bf90f45d73746d69959d33620a3df5518"]
}]
Or by network and height with #network and #height, or everything a key has attested with authors.
Verification
- The event id and signature, per NIP-01.
- The kind is 3332 and the content parses as a statement whose canonical form (RFC 8785) equals the content byte for byte.
- The required fields are present;
methodobeys the rules above; the note carries no link. - Every tag that repeats a statement field agrees with the content.
- With a node of the named chain: the block hash is on its best chain at the stated height, and its genesis matches.
- Against a snapshot entry (chainparams, a record, a torrent's metadata): the subject's
txoutsetHash,coinsandchainTxCountmatch.
Counts never promote a hash. Many attestations of the same hash are evidence only in proportion to what each attester did, and a single recomputation that disagrees is a dispute to be settled by recomputation, not by majority.
Corrections
An attestation is not replaced. A signer who finds their attestation wrong publishes a new kind 3332 event with an e tag
["e", "<id of the old event>", "", "correction"] and a statement of what they now find. Readers show both. Deletion requests
(NIP-09) are advisory and do not remove an attestation from the record.
Disputes
A Dispute is an attestation that disagrees. Kind 3331, a regular event like 3332, one number down so a client can subscribe to
disputes alone. The content is a statement of @type Dispute, held to every rule an Attestation is held to, so a dispute costs the
same work and carries the same risk: a dispute later shown wrong is a wrong row on the disputer.
{
"v": "0.0.1",
"@type": "Dispute",
"subject": {
"network": "tbtc4",
"genesis": "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043",
"height": 150000,
"blockHash": "0000000000d9877342754dea8ec1eb24631517d38e3443c370465ee53a8b7434",
"txoutsetHash": "<the hash this node got>",
"hashType": "hash_serialized_3",
"coins": 14228512,
"chainTxCount": 14810011
},
"disputes": {
"txoutsetHash": "<the hash it contradicts>",
"attestations": ["<event id>", "<event id>"]
},
"claim": { "method": "recomputed", "chainstate": "full", "validated": true, "implementation": "…", "computedAt": "…" },
"proof": { "proofType": "recompute" }
}
subjectis this node's own result.disputes.txoutsetHashis the hash it contradicts, anddisputes.attestationsthe ids of the attestation events that stated it; the envelope repeats them asetags so relays can find every dispute of an event, and atxoutset_hashtag carries the disputed hash so a client can find every dispute of a hash.- Only
recomputedmay dispute. A node that loaded the snapshot has nothing to dispute with. proof.proofTypesays what evidence there is.recompute: a full run disagrees; the numbers are the evidence.piece: evidence against a file rather than a hash:{ "proofType": "piece", "infohash", "index", "expected", "got" }, the torrent's piece hash from the metainfo and the hash of the bytes actually served, which anyone checks against the torrent.coinis reserved for when the set is committed to as a tree and a wrong coin can be shown in a few hundred bytes.- What a record does with it: the disputed hash shows as disputed, with both sides' keys and provenance; nothing is removed; the dispute is settled only by further recomputation, and whichever side the recomputations go against keeps the rows it signed.
Security Considerations
Attesting costs nothing, which is the point: the cost is being wrong in public. A reader weighing attestations should weigh the
claim's provenance (recomputed on a full chainstate is the most; loaded is less; anything on a chainstate rooted in a snapshot
at or above the height is nothing), the key's history, and whether any fraud proof names the hash. Self-reported software fields
prove nothing on their own. A relay may drop or withhold events; a verifier that cares keeps its own copies.
Starting a reputation. A new key has no history, and attesting costs nothing, so a reader has little to weigh. Work in the key is a starting point: a public key with leading zero bits was expensive to make, in proportion to the bits, and any reader can count them without anything new on the wire (a tool such as rana mines one). NIP-13 work on the event is the per-claim equivalent. Either is a column beside method and history, never a promotion of the hash; a record SHOULD show it and MUST NOT require it, since the point is to let anyone with a node attest.
Subkeys and controllers
A node signs with its own key, so the attestation stays the node's and a wrong row lands on the key that signed it. To let a reader walk from a node key to the person who runs it, both sides say so, and a reader trusts the link only when both verify:
- The controller's side: a NIP-51 set from the controller's key,
kind
30000withdofkeys, each node key aptag with a role and a label:["p", "<node pubkey>", "", "node", "shadow txbt4"]. Replaceable is right here: the set is current state, and removing a key revokes it. - The subkey's side: the node key's kind
0profile carries"controller": "did:nostr:<controller pubkey>". - What a record shows: "controlled by" only when the profile names the controller and the controller's set names the key. Reputation aggregates upward to the controller; blame stays where the signature is, and on the controller who claimed the key.
A controller may be the same key as the node's: a person signing from their own browser needs no subkey at all, and their own key is the best key, since its history already exists. A GitHub identity attached through NIP-39 lets a reader join informal attestations made under a GitHub name to formal ones made under the key.
Implementation
- Signing from a node: play-grounds/assumeutxo
sign/reads the facts, the implementation and the chainstate from the running node over RPC and refuses anything typed. - Signing in a browser: the sign page, NIP-07 or a pasted key.
- Verifying:
sign/verify.mjsin the same repository, steps 1 to 5 above. - A record that collects them: assumeutxo.com.
References
- NIP-01: event format and kind ranges
- NIP-333: Bitcoin block headers over Nostr, the sibling of this NIP
- BIP 14: the user agent string
- RFC 8785: JSON Canonicalization Scheme
- Bitcoin Core assumeutxo design
- in-toto attestation framework: subject and predicate, the shape this statement follows
Related work
- NIP-85, Trusted Assertions: providers publish ranked assertions about keys and events for clients that delegate trust; opinions about keys, where this NIP carries recomputed facts. A record of attestations could act as a provider.
- NIP-VA, Verifiable Attestations (draft, kind 31000): typed claims about keys, assertion-first, revoked by replacement; this NIP is permanent by design.
- Attestr (Nosfabrica Wotathon 2026): an attestation as an event about the validity of any other event, trust by lists and delegation. Same word, different object: trust by whom you follow, here by what was recomputed.
- NIP-03, OpenTimestamps attestations: proves when an event existed, not whether its claim holds; an attestation under this NIP may carry one.
Cited links
nip-332.github.io
NIP-332 Specification
github.com
nip-332.github.io/spec/nip-332.md at gh-pages · nip-332/nip-332.github.io
NIP-332: assumeUTXO Attestations over Nostr. Contribute to nip-332/nip-332.github.io development by creating an account on GitHub.
rfc-editor.org
RFC 8785: JSON Canonicalization Scheme (JCS) | RFC Editor
Cryptographic operations like hashing and signing need the data to be expressed in an invariant format so that the operations are reliably repeatable. One way to address this is to create a canonic...
github.com
GitHub - grunch/rana: Nostr public key mining tool
Nostr public key mining tool. Contribute to grunch/rana development by creating an account on GitHub.
github.com
nips/51.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
github.com
nips/39.md at master · nostr-protocol/nips
Nostr Implementation Possibilities. Contribute to nostr-protocol/nips development by creating an account on GitHub.
Discussion
Connect a key to comment.