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

`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>
This commit is contained in:
Padreug 2026-06-03 19:05:03 +02:00
commit 59e90d07c0

View file

@ -45,12 +45,36 @@ export class Backend extends NDKNip46Backend {
async start(): Promise<void> {
this.localUser = await this.signer.user();
await new Promise<void>((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());