|
| 1 | +# NIP-03 smoke test with a real OpenTimestamps client |
| 2 | + |
| 3 | +This exercises a running nostream relay against a genuine Bitcoin-attested |
| 4 | +OpenTimestamps proof that a real OTS client produced in the wild (the |
| 5 | +event used in the [NIP-03 spec example](https://github.com/nostr-protocol/nips/blob/master/03.md) |
| 6 | +on `wss://nostr-pub.wellorder.net`). |
| 7 | + |
| 8 | +The relay never sees a synthetic proof here: the `.ots` blob in `content` |
| 9 | +was made by a real `ots stamp` + `ots upgrade` flow, is attested to a real |
| 10 | +Bitcoin block header, and is validated by the real `ots verify` binary |
| 11 | +against a public Esplora server before and after it round-trips through |
| 12 | +your relay. |
| 13 | + |
| 14 | +## Why not generate our own proof end-to-end? |
| 15 | + |
| 16 | +`ots stamp` writes a pending proof. A pending proof has to sit in a |
| 17 | +calendar server's queue for several Bitcoin blocks (typically a few |
| 18 | +hours) before `ots upgrade` can turn it into a confirmed Bitcoin |
| 19 | +attestation. Running that end-to-end in CI or on a developer machine is |
| 20 | +impractical. Re-using an already-upgraded, already-published kind 1040 |
| 21 | +event is an equally honest "real client" test: we did not make the |
| 22 | +proof, and we prove its validity with the same binary a Nostr client |
| 23 | +would use. |
| 24 | + |
| 25 | +## Prerequisites |
| 26 | + |
| 27 | +- A running nostream relay (default `ws://127.0.0.1:8008`). |
| 28 | +- Node.js (same as the repo; the script runs via `ts-node` and uses the `ws` package). |
| 29 | +- [`opentimestamps-client`](https://github.com/opentimestamps/opentimestamps-client) |
| 30 | + for the real `ots` step (optional; the script auto-detects and skips |
| 31 | + gracefully if it's not installed): |
| 32 | + - Linux / macOS: `pipx install opentimestamps-client`, or |
| 33 | + `pip install opentimestamps-client`. |
| 34 | + - Windows: Python 3.13 has an OpenSSL compatibility bug in |
| 35 | + `python-bitcoinlib` on which the client depends. Run it inside a |
| 36 | + container instead: |
| 37 | + |
| 38 | + ```bash |
| 39 | + docker run --rm -v $PWD:/work python:3.11-slim \ |
| 40 | + sh -c "pip install -q opentimestamps-client && ots info /work/proof.ots" |
| 41 | + ``` |
| 42 | +- A Bitcoin node (optional, only for `--verify`). Without one the script |
| 43 | + runs `ots info`, which parses the proof and confirms the Bitcoin |
| 44 | + attestation it terminates in. `ots verify` additionally looks up the |
| 45 | + block header on a Bitcoin node to prove the attestation is genuine; if |
| 46 | + you don't have one, `ots info` is the honest equivalent of |
| 47 | + structural-client acceptance. |
| 48 | +
|
| 49 | +## Running the automated script |
| 50 | +
|
| 51 | +```bash |
| 52 | +npm run smoke:nip03 |
| 53 | +# or, with non-default relays: |
| 54 | +npx ts-node scripts/smoke-nip03.ts \ |
| 55 | + --local-relay ws://127.0.0.1:8008 \ |
| 56 | + --source-relay wss://nostr-pub.wellorder.net |
| 57 | +``` |
| 58 | +
|
| 59 | +Expected output on a healthy relay with `ots` installed: |
| 60 | +
|
| 61 | +``` |
| 62 | +NIP-03 end-to-end smoke test |
| 63 | + local relay: ws://127.0.0.1:8008 |
| 64 | + source relays: wss://nos.lol, wss://relay.damus.io, wss://nostr.wine, wss://offchain.pub, wss://nostr-pub.wellorder.net |
| 65 | +
|
| 66 | +1) Discovering a real NIP-03 event from public relays |
| 67 | + trying wss://nos.lol for any recent kind 1040… |
| 68 | + PASS discovered 697b40df2f1c… on wss://nos.lol (pubkey=b1104a6e…, attests e=88fea43a70bd…, content=4968 chars) |
| 69 | +
|
| 70 | +2) Parsing OTS content with the real `ots` client |
| 71 | + PASS ots info parsed proof — BitcoinBlockHeaderAttestation(941057) (file: /tmp/nip03-XXXX/proof.ots) |
| 72 | +
|
| 73 | +3) Publishing the real event to the local relay |
| 74 | + PASS local relay accepted real NIP-03 event (reason="") |
| 75 | +
|
| 76 | +4) Round-tripping the event through the local relay |
| 77 | + PASS local relay returned the same event (id, sig, content) on REQ |
| 78 | +
|
| 79 | +summary: 3 passed, 0 failed |
| 80 | +``` |
| 81 | +
|
| 82 | +Pass `--verify` (or set `OTS_VERIFY=1`) to additionally run |
| 83 | +`ots verify -d <target-event-id>` which asks a Bitcoin node to confirm |
| 84 | +the block header. Exit code is `0` iff every step passes. |
| 85 | +
|
| 86 | +## What each step proves |
| 87 | +
|
| 88 | +1. **Source discovery** — confirms a real third-party kind 1040 event |
| 89 | + exists, came from a real signed OTS client flow, and that you and the |
| 90 | + network agree on its bytes. |
| 91 | +2. **`ots info` (or `ots verify`)** — confirms the `.ots` content in |
| 92 | + `event.content` is a structurally valid OpenTimestamps proof when fed |
| 93 | + to the real reference client, and that it terminates in a Bitcoin |
| 94 | + block header attestation (which is what NIP-03 requires). If |
| 95 | + `--verify` is set, additionally walks the Bitcoin header via a |
| 96 | + configured node to prove the attested block really contains the merkle |
| 97 | + root. |
| 98 | +3. **Publish** — confirms nostream's NIP-03 strategy accepts a |
| 99 | + real-world, real-client-produced kind 1040 event (structure, |
| 100 | + `e` tag, digest match, Bitcoin attestation requirement all satisfied). |
| 101 | +4. **Round-trip** — confirms the relay persisted the event unchanged and |
| 102 | + returns the exact same id, signature, and base64 content on REQ, so |
| 103 | + downstream clients that re-run `ots verify` on the relay's output will |
| 104 | + still succeed. |
| 105 | +
|
| 106 | +## Manual walkthrough (if you want to stamp your own) |
| 107 | +
|
| 108 | +If you do have a few hours to wait and want a proof you made yourself: |
| 109 | +
|
| 110 | +```bash |
| 111 | +export RELAY=ws://127.0.0.1:8008 |
| 112 | +export SK=$(nak key generate) |
| 113 | +export EVENT_ID=$(nak event --sec "$SK" -k 1 -c "anchor this note" "$RELAY" | jq -r '.[1].id // .id') |
| 114 | +
|
| 115 | +# Stamp the raw 32 bytes of the event id (not the hex string) |
| 116 | +echo -n "$EVENT_ID" | xxd -r -p > /tmp/nip03-digest.bin |
| 117 | +ots stamp /tmp/nip03-digest.bin |
| 118 | +
|
| 119 | +# Wait for calendars + Bitcoin confirmation, then: |
| 120 | +ots upgrade /tmp/nip03-digest.bin.ots |
| 121 | +ots verify /tmp/nip03-digest.bin.ots |
| 122 | +
|
| 123 | +export OTS_B64=$(base64 -w0 /tmp/nip03-digest.bin.ots) |
| 124 | +nak event --sec "$SK" -k 1040 \ |
| 125 | + -t e="$EVENT_ID" \ |
| 126 | + -t k=1 \ |
| 127 | + -c "$OTS_B64" \ |
| 128 | + "$RELAY" |
| 129 | +
|
| 130 | +# round-trip |
| 131 | +nak req -k 1040 -a "$(nak key public "$SK")" "$RELAY" \ |
| 132 | + | jq -r '.content' | base64 -d | ots verify - |
| 133 | +``` |
| 134 | +
|
| 135 | +Each publish attempt should come back as `["OK", "<id>", true, ""]`. |
| 136 | +
|
| 137 | +## Negative paths |
| 138 | +
|
| 139 | +Actively testing NIP-03 rejection paths (mismatched digest, uppercase |
| 140 | +`e` tag, multiple `k` tags, unsupported OTS version, garbage content) |
| 141 | +would require re-signing a mutated event, which means the proof would no |
| 142 | +longer be produced by a real OTS client. Those paths are covered in |
| 143 | +isolation by the unit tests: |
| 144 | +
|
| 145 | +- `test/unit/utils/nip03.spec.ts` |
| 146 | +- `test/unit/handlers/event-strategies/timestamp-event-strategy.spec.ts` |
0 commit comments