From 59e90d07c09862a709475b02c69d4c6f4c7bd462 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 3 Jun 2026 19:05:03 +0200 Subject: [PATCH] fix(backend): pin per-key kind:24133 subscription to explicit relays (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Backend.start()` calls `this.ndk.subscribe(filter, opts)` to listen for NIP-46 events targeted at each unlocked key's pubkey (kind:24133 with `#p=[localUser.pubkey]`). Pre-fix this subscription opts didn't pin a relay set, so NDK 3.x's outbox routing kicked in: it looked up the `localUser.pubkey`'s NIP-65 relay list (kind:10002) to decide where to send the REQ. Newly-provisioned bunker keys have no kind:10002 published yet, so NDK's subscription manager queued the REQ waiting for a relay list that would never arrive — the subscription never landed on the wire. The user-visible symptom: every NIP-46 RPC from lnbits to a freshly- provisioned key (`connect`, `get_public_key`, `sign_event`, the `nip04_*` / `nip44_*` family) was published into the relay, the relay tried to route, found no subscribed peer matching `["p", new_key_pubkey]`, and emitted "Filter didn't match". The lnbits-side RPC then timed out at 15s, breaking eager merchant provisioning (aiolabs/lnbits#46) and satmachineadmin's per-cassette `nip44_decrypt` polling. Reproduced + diagnosed by patching the lnbits `nostrrelay` extension's `_handle_request` to log incoming REQ filters: only the admin subscription (`{kinds:[24133,24134], #p:[bunker_admin_pubkey]}` from `AdminInterface.connect()`) appeared on the wire. The per-key Backend filters from `Backend.start()` did not. Fix: pass `relayUrls: this.ndk.explicitRelayUrls` in the subscription opts. `relayUrls` was added in NDK 2.13.0 as the supported way to bypass outbox routing per subscription; the relay set built from these URLs matches what the rest of the daemon uses (admin RPC channel + every per-key Backend channel), so events flow through the same connection the admin interface already established. Verified on the regtest dev stack with bunker enabled, fresh signup provisioning a new key + immediately publishing a kind:30017 stall via NIP-46 sign_event: POST /auth/register → HTTP 200 in 1.1s stalls.event_id = 8a2eb20b929… (populated by bunker signature) relay sees: nostr event: [30017, , '{...store...}'] Pre-fix this same flow timed out at `NsecBunkerTimeoutError: no NIP-46 response for 'sign_event' within 15.0s`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/daemon/backend/index.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/daemon/backend/index.ts b/src/daemon/backend/index.ts index 49112fc..5d05a8a 100644 --- a/src/daemon/backend/index.ts +++ b/src/daemon/backend/index.ts @@ -45,12 +45,36 @@ export class Backend extends NDKNip46Backend { async start(): Promise { this.localUser = await this.signer.user(); await new Promise((resolve) => { + // Pin this subscription to the daemon's explicit relays via + // `relayUrls`. Without that, NDK 3.x's outbox routing tries to + // resolve the relay set from `this.localUser.pubkey`'s NIP-65 + // relay list (kind:10002). Newly-provisioned bunker keys have + // no published kind:10002 yet, so NDK's subscription manager + // queues the REQ waiting for a relay list that will never + // arrive — the kind:24133 subscription never lands on the + // wire, and inbound NIP-46 events (sign_event, get_public_key, + // nip44_*) targeted at this key get dropped by the relay + // with "Filter didn't match" because the bunker isn't actually + // subscribed for them. + // + // `relayUrls` was added in NDK 2.13.0 as the supported way to + // bypass outbox routing per subscription (see + // NDKSubscriptionOptions.relayUrls in @nostr-dev-kit/ndk). + // The relay set built from these URLs matches what the rest + // of the bunker uses (admin RPC channel + per-key Backend + // channels alike), so events flow through the same connection + // the admin interface already established. + // + // See aiolabs/nsecbunkerd#21. const sub = this.ndk.subscribe( { kinds: [24133], "#p": [this.localUser!.pubkey], }, - { closeOnEose: false } + { + closeOnEose: false, + relayUrls: this.ndk.explicitRelayUrls, + } ); sub.on("event", (e: any) => this.handleIncomingEvent(e)); sub.on("eose", () => resolve()); -- 2.53.0