# 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](https://www.rfc-editor.org/rfc/rfc8785)), 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).

```json
{
  "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

```json
{
  "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

```json
["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.

```json
{
  "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](https://github.com/grunch/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](https://github.com/nostr-protocol/nips/blob/master/51.md) 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](https://github.com/nostr-protocol/nips/blob/master/39.md)
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/`](https://github.com/play-grounds/assumeutxo/tree/gh-pages/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](https://play-grounds.github.io/assumeutxo/sign/), 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](https://assumeutxo.com/).

## References

- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md): event format and kind ranges
- [NIP-333](https://nip-333.github.io/): Bitcoin block headers over Nostr, the sibling of this NIP
- [BIP 14](https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki): the user agent string
- [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785): JSON Canonicalization Scheme
- [Bitcoin Core assumeutxo design](https://github.com/bitcoin/bitcoin/blob/master/doc/design/assumeutxo.md)
- [in-toto attestation framework](https://github.com/in-toto/attestation): subject and predicate, the shape this statement follows

### Related work

- [NIP-85](https://github.com/nostr-protocol/nips/blob/master/85.md), 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](https://github.com/forgesworn/nostr-attestations/blob/HEAD/NIP-VA.md), Verifiable Attestations (draft, kind 31000): typed claims about keys, assertion-first, revoked by replacement; this NIP is permanent by design.
- [Attestr](https://nathan.day.ag/presentations/nosfabrica-wotathon-2026-attestr-final-pitch/) (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](https://github.com/nostr-protocol/nips/blob/master/03.md), OpenTimestamps attestations: proves when an event existed, not whether its claim holds; an attestation under this NIP may carry one.
