Carry the operator identity in the pairing seed (NIP-19 nprofile) #62

Open
opened 2026-06-21 09:31:32 +00:00 by padreug · 0 comments
Owner

Problem

The pairing seed (spire-seed:v1:, #52) carries the spire identity + relays + bunker URL, but the operator identity is still provisioned separately as VITE_OPERATOR_PUBKEYS. During the 2026-06-21 Sintra bunker smoke we had to hand-supply the operator pubkey (3d8eabbf…) alongside the seed. That's a second out-of-band value, and the end-goal is a single QR scan that fully provisions the machine.

The operator pubkey isn't cosmetic — operator-config.ts / operator-fees.ts subscribe to the operator's kind-30078 fee/cassette config with authors: [operatorPubkey], and the cassette-state hello is nip44_encrypt'd to it. Without it the machine can't receive operator config.

Proposal — put the operator in the seed as a NIP-19 nprofile

nprofile (NIP-19) is the canonical nostr primitive for "a pubkey + where to find them": a bech32 TLV with the 32-byte pubkey (type 0) + optional relay hints (type 1). Add it to the seed JSON:

// spire-seed:v2 (or additive-optional on v1)
{
  "v": 2,
  "spire_pubkey": "…",
  "bunker_url": "bunker://…",
  "relays": ["wss://…"],            // spire's OWN event relays
  "operator": "nprofile1…"          // NEW: operator pubkey + the operator's relay hints
}

Why nprofile specifically (vs. a bare operator_pubkey hex):

  1. Identity travels with the seed — no separate VITE_OPERATOR_PUBKEYS to provision; one artifact, one eventual QR.
  2. Relay hints tell the ATM where to subscribe for operator-published config. Today the ATM subscribes for operator events on its own VITE_RELAY_URL; the operator may publish elsewhere. The nprofile's relay TLV is exactly this hint (NIP-19 §1: relay).
  3. Standard + tooling-ready — nostr-tools nip19.nprofileEncode/decode on both sides; nothing bespoke.

Work

Producer (aiolabs/spirekeeper, pairing.py): include operator: nprofileEncode({pubkey, relays}) when minting (pair_spire); seed-contract version bump. The operator pubkey + its relays are already known operator-side.

Consumer (aiolabs/bitspire):

  • seed.ts — parse the operator nprofile → { pubkey, relays }.
  • signer-resolver.ts / lightning.ts — feed the operator pubkey into operatorPubkeys and the relay hints into the operator-config/fees subscriptions; persist on the bunker_binding.
  • Backwards-compatible: fall back to VITE_OPERATOR_PUBKEYS when the seed has no operator (v1 seeds).

Robustness sub-task (independent, small): normalize VITE_OPERATOR_PUBKEYS through parsePublicKey (already in identity.ts, handles hex/npub/nprofile → hex) so operators can paste any form. Today it's a raw .split(',') and an npub silently matches nothing (confirmed during the smoke — the value must be 64-hex).

Refs

  • NIP-19 nprofile — refs/repos/nostr/nostr-protocol/nips/19.md
  • Surfaced during the 2026-06-21 Sintra smoke (~/dev/coordination/smoke-bunker-pairing-sintra.md); legs 1+2 (pair + resume) passed with the operator hand-provisioned.
  • Parent: #52. Producer-side: aiolabs/spirekeeper.
## Problem The pairing seed (`spire-seed:v1:`, #52) carries the **spire** identity + relays + bunker URL, but the **operator** identity is still provisioned separately as `VITE_OPERATOR_PUBKEYS`. During the 2026-06-21 Sintra bunker smoke we had to hand-supply the operator pubkey (`3d8eabbf…`) alongside the seed. That's a second out-of-band value, and the end-goal is a **single QR scan** that fully provisions the machine. The operator pubkey isn't cosmetic — `operator-config.ts` / `operator-fees.ts` subscribe to the operator's kind-30078 fee/cassette config with `authors: [operatorPubkey]`, and the cassette-state hello is `nip44_encrypt`'d to it. Without it the machine can't receive operator config. ## Proposal — put the operator in the seed as a NIP-19 `nprofile` `nprofile` (NIP-19) is the canonical nostr primitive for "a pubkey **+ where to find them**": a bech32 TLV with the 32-byte pubkey (type 0) + optional relay hints (type 1). Add it to the seed JSON: ```jsonc // spire-seed:v2 (or additive-optional on v1) { "v": 2, "spire_pubkey": "…", "bunker_url": "bunker://…", "relays": ["wss://…"], // spire's OWN event relays "operator": "nprofile1…" // NEW: operator pubkey + the operator's relay hints } ``` Why `nprofile` specifically (vs. a bare `operator_pubkey` hex): 1. **Identity travels with the seed** — no separate `VITE_OPERATOR_PUBKEYS` to provision; one artifact, one eventual QR. 2. **Relay hints tell the ATM *where* to subscribe** for operator-published config. Today the ATM subscribes for operator events on its *own* `VITE_RELAY_URL`; the operator may publish elsewhere. The nprofile's relay TLV is exactly this hint (NIP-19 §`1: relay`). 3. **Standard + tooling-ready** — `nostr-tools` `nip19.nprofileEncode`/`decode` on both sides; nothing bespoke. ## Work **Producer (`aiolabs/spirekeeper`, `pairing.py`):** include `operator: nprofileEncode({pubkey, relays})` when minting (`pair_spire`); seed-contract version bump. The operator pubkey + its relays are already known operator-side. **Consumer (`aiolabs/bitspire`):** - `seed.ts` — parse the `operator` nprofile → `{ pubkey, relays }`. - `signer-resolver.ts` / `lightning.ts` — feed the operator pubkey into `operatorPubkeys` and the relay hints into the operator-config/fees subscriptions; persist on the `bunker_binding`. - Backwards-compatible: fall back to `VITE_OPERATOR_PUBKEYS` when the seed has no `operator` (v1 seeds). **Robustness sub-task (independent, small):** normalize `VITE_OPERATOR_PUBKEYS` through `parsePublicKey` (already in `identity.ts`, handles hex/npub/nprofile → hex) so operators can paste any form. Today it's a raw `.split(',')` and an npub silently matches nothing (confirmed during the smoke — the value must be 64-hex). ## Refs - NIP-19 `nprofile` — `refs/repos/nostr/nostr-protocol/nips/19.md` - Surfaced during the 2026-06-21 Sintra smoke (`~/dev/coordination/smoke-bunker-pairing-sintra.md`); legs 1+2 (pair + resume) passed with the operator hand-provisioned. - Parent: #52. Producer-side: `aiolabs/spirekeeper`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire#62
No description provided.