30817:nip-332-assumeutxo-attestations

NIP-332: assumeUTXO Attestations

Melvin Carvalho

published
2026-10-16

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

  1. The event id and signature, per NIP-01.
  2. The kind is 3332 and the content parses as a statement whose canonical form (RFC 8785) equals the content byte for byte.
  3. The required fields are present; method obeys the rules above; the note carries no link.
  4. Every tag that repeats a statement field agrees with the content.
  5. With a node of the named chain: the block hash is on its best chain at the stated height, and its genesis matches.
  6. Against a snapshot entry (chainparams, a record, a torrent's metadata): the subject's txoutsetHash, coins and chainTxCount match.

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" }
}
  • subject is this node's own result. disputes.txoutsetHash is the hash it contradicts, and disputes.attestations the ids of the attestation events that stated it; the envelope repeats them as e tags so relays can find every dispute of an event, and a txoutset_hash tag carries the disputed hash so a client can find every dispute of a hash.
  • Only recomputed may dispute. A node that loaded the snapshot has nothing to dispute with.
  • proof.proofType says 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. coin is 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 30000 with d of keys, each node key a p tag 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 0 profile 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.mjs in the same repository, steps 1 to 5 above.
  • A record that collects them: assumeutxo.com.

References

  • 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

Discussion

Connect a key to comment.