Watchdog reconnect re-opens the socket but never replays subscriptions → bunker goes silently deaf after a relay flap #41
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
After a relay disconnect/flap,
attachIndefiniteReconnect(src/daemon/lib/relay-reconnect.ts) re-establishes the WebSocket viarelay.connect(), but the NIP-46 subscriptions are never replayed. The bunker ends up connected but deaf: it holds an open socket to the relay yet has no activeREQ, so every incoming signing request (NIP-46 kind-24133) is delivered to a relay the bunker is no longer subscribed on. Allsign_event/nip44_encrypt/ etc. requests time out client-side (no response in 10000ms) until the daemon is manually restarted. This affects every key, not one binding.Hit in production (demo) — twice
nsecbunkerd 0.10.5onaio-demo, single backend relayws://127.0.0.1:5000/nostrrelay/demo.FLAPPING; the watchdog manually reconnects the socket; the daemon then logs nothing for any key while a bitSpire ATM retriesnip44_encryptevery ~10s for hours (BunkerTimeoutError: no response in 10000ms, "signer unreachable").nsecbunkerdfully recovers it — fresh✅ Connected,🔓 autounlockfor all keys, subscriptions re-established. Nothing about the keys/tokens/bindings/seed was wrong; purely the stale post-reconnect subscription. Recurred a second time on the next flap.Why this keeps coming back — it's the seam between #20 and #21
This problem has been fixed at four different layers, and #41 is the one seam none of them closed:
pingOrDie→ pool-status).isFlapping()give-up + outbox routing (and thus #20 + #21).attachIndefiniteReconnect) — connection persistence: keep re-opening the socket past NDK's give-up.relayUrlsso it lands at startup.#21 makes the subscription land at startup; #20 re-opens the socket after a drop. But when NDK declares the relay FLAPPING it tears down the live
NDKSubscription, and #20's barerelay.connect()re-opens the socket without re-issuing the REQ. Every fix treated the transport (socket up? REQ routed right?); none treated the session (after a teardown, is the REQ re-issued?).…and #20 blinded the safety net that used to catch it
The original
pingOrDiewatchdog published an event and waited to receive it back through a subscription — a real end-to-end liveness check that would have caught a dead subscription on a live socket. It false-positived on non-echoing relays, so it was replaced (#4) with the cheap pool-status check (ndk.pool.connectedRelays().length,admin/index.ts:518). Once #20 pins the socket up,connectedRelays()is always ≥1 → the watchdog never fires → the deaf state persists forever. Before #20, a flap that killed the subscription eventually starvedconnectedRelays()to 0 and the watchdog restarted the process (re-subscribing viastartKeys/autounlock). After #20 it limps along connected-but-deaf. The regression hides inside the fix.Fix (two complementary parts)
relay:connectrecovery path (n > 0) inrelay-reconnect.ts, re-run the daemon's subscription setup — Backend's per-key kind:24133 subscriptions and the admin subscription — rather than trusting NDK to replay them. Plumb anonRecovercallback so the supervisor can trigger the daemon/admin re-subscribe without coupling the generic helper to daemon internals.Acceptance
After a forced relay flap mid-session (relay drops, comes back), the bunker resumes serving signing requests without a manual restart. Regression test: flap the relay during an active session and assert a subsequent
sign_eventstill gets answered. Without (1) the next NDK upgrade/relay quirk reopens the hole; without (2) we stay blind to it when it does.Severity
High. Silent: connection status looks healthy, signing just stops. A bitSpire ATM sat in "signer unreachable" maintenance for hours, twice.
refs:
src/daemon/lib/relay-reconnect.ts,src/daemon/admin/index.ts:489-525; #20 (the reconnect watchdog this extends), #21 (the subscription-routing fix this is the runtime sibling of); demo incidents 2026-06-23 + 2026-06-26.