fix(backend): pin per-key kind:24133 subscription to explicit relays (#21) #23

Merged
padreug merged 1 commit from fix-21-pin-backend-sub-to-explicit-relays into dev 2026-06-03 17:07:23 +00:00
Owner

Summary

Closes #21. Fix the second half of the bunker-side regression diagnosed in the 2026-06-03 debug session (companion to PR #22 / #20).

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 the subscription opts didn't pin a relay set, so NDK 3.x's outbox routing took over: 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.

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.

Diagnosis

Reproduced + confirmed by patching the lnbits nostrrelay extension's _handle_request to log every incoming REQ filter. After a signup:

  • Admin sub from AdminInterface.connect() (admin/index.ts:145) appeared on the wire:
    [NOSTRRELAY REQ] sub='ntdsml-n1fmy' filter={'p': ['fb90174c…admin'], 'kinds': [24133, 24134]}
    
  • Per-key Backend sub from Backend.start() for the newly-provisioned key did not appear at all.

So the AdminInterface's subscription (which has the admin signer's pubkey, and the daemon has explicit relays configured for it) was reaching the wire; the per-key Backend's subscription (using the same this.ndk instance, with #p pointing at a key whose outbox is unresolved) was 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 alike), so events flow through the same connection the AdminInterface already established.

 const sub = this.ndk.subscribe(
     {
         kinds: [24133],
         "#p": [this.localUser!.pubkey],
     },
-    { closeOnEose: false }
+    {
+        closeOnEose: false,
+        relayUrls: this.ndk.explicitRelayUrls,
+    }
 );

Test plan

Verified on the regtest dev stack with bunker enabled, fresh signup that provisions a new key + immediately publishes a kind:30017 stall via NIP-46 sign_event:

  • POST /api/v1/auth/registerHTTP 200 in 1.1 s (pre-fix: hung at 15 s with NsecBunkerTimeoutError: no NIP-46 response for 'sign_event').
  • stalls.event_id populated in ext_nostrmarket.sqlite3: 8a2eb20b929… — proves the bunker signed and returned the event to lnbits.
  • Relay log shows the stall event published with the new user pubkey as author:
    nostr event: [30017, edc7c5b007aa8…, '{"id":"…","name":"Pinsubs_207\\'s Store",…}']
    
  • pnpm run build clean.
  • To verify: long-running soak with many subsequent NIP-46 RPCs on the same key (sign + crypto), 10+ accounts, no flakes.

Notes

  • The admin subscription doesn't need this fix because it doesn't carry a #p filter for a pubkey whose outbox is unresolved — the AdminInterface's NDK has the admin signer set, so outbox resolution doesn't fire the same blocking lookup. But applying relayUrls to the admin sub too would be a small additional belt-and-suspenders if a future NDK version changes that behavior; out of scope here.
  • Cross-references: this is the second half of the bunker-side fix needed for the regtest signup-with-bunker path to work end-to-end. The first half (NDK retry give-up) merged in #22. The lnbits-side DEFAULT_POLICY_RULES NIP-15 kind fix landed in aiolabs/lnbits#48. The nostrmarket publish-without-timeout fix landed in aiolabs/nostrmarket#8.

🤖 Generated with Claude Code

## Summary Closes #21. Fix the second half of the bunker-side regression diagnosed in the 2026-06-03 debug session (companion to PR #22 / #20). `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 the subscription opts didn't pin a relay set, so NDK 3.x's outbox routing took over: 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.** 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. ## Diagnosis Reproduced + confirmed by patching the lnbits `nostrrelay` extension's `_handle_request` to log every incoming REQ filter. After a signup: - Admin sub from `AdminInterface.connect()` (admin/index.ts:145) appeared on the wire: ``` [NOSTRRELAY REQ] sub='ntdsml-n1fmy' filter={'p': ['fb90174c…admin'], 'kinds': [24133, 24134]} ``` - Per-key Backend sub from `Backend.start()` for the newly-provisioned key **did not appear at all**. So the AdminInterface's subscription (which has the admin signer's pubkey, and the daemon has explicit relays configured for it) was reaching the wire; the per-key Backend's subscription (using the same `this.ndk` instance, with `#p` pointing at a key whose outbox is unresolved) was 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 alike), so events flow through the same connection the AdminInterface already established. ```diff const sub = this.ndk.subscribe( { kinds: [24133], "#p": [this.localUser!.pubkey], }, - { closeOnEose: false } + { + closeOnEose: false, + relayUrls: this.ndk.explicitRelayUrls, + } ); ``` ## Test plan Verified on the regtest dev stack with bunker enabled, fresh signup that provisions a new key + immediately publishes a kind:30017 stall via NIP-46 `sign_event`: - [x] `POST /api/v1/auth/register` → `HTTP 200` in **1.1 s** (pre-fix: hung at 15 s with `NsecBunkerTimeoutError: no NIP-46 response for 'sign_event'`). - [x] `stalls.event_id` populated in `ext_nostrmarket.sqlite3`: `8a2eb20b929…` — proves the bunker signed and returned the event to lnbits. - [x] Relay log shows the stall event published with the new user pubkey as author: ``` nostr event: [30017, edc7c5b007aa8…, '{"id":"…","name":"Pinsubs_207\\'s Store",…}'] ``` - [x] `pnpm run build` clean. - [ ] To verify: long-running soak with many subsequent NIP-46 RPCs on the same key (sign + crypto), 10+ accounts, no flakes. ## Notes - The admin subscription doesn't need this fix because it doesn't carry a `#p` filter for a pubkey whose outbox is unresolved — the AdminInterface's NDK has the admin signer set, so outbox resolution doesn't fire the same blocking lookup. But applying `relayUrls` to the admin sub too would be a small additional belt-and-suspenders if a future NDK version changes that behavior; out of scope here. - Cross-references: this is the second half of the bunker-side fix needed for the regtest signup-with-bunker path to work end-to-end. The first half (NDK retry give-up) merged in #22. The lnbits-side `DEFAULT_POLICY_RULES` NIP-15 kind fix landed in aiolabs/lnbits#48. The nostrmarket publish-without-timeout fix landed in aiolabs/nostrmarket#8. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(backend): pin per-key kind:24133 subscription to explicit relays (#21)
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
59e90d07c0
`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, <new-key-pubkey>, '{...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) <noreply@anthropic.com>
padreug deleted branch fix-21-pin-backend-sub-to-explicit-relays 2026-06-03 17:07:23 +00:00
Sign in to join this conversation.
No reviewers
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/nsecbunkerd!23
No description provided.