fix(#9): close race between create_new_key and NIP-46 connect #10
2 changed files with 40 additions and 3 deletions
fix(#9): close race between create_new_key and NIP-46 connect
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Two-layer fix for the issue where a fresh client chaining create_new_key + NIP-46 connect on the same target key would time out — bunker had no subscription registered for the new key by the time the connect event arrived at the relay. Layer 1 — run.ts: loadNsec and unlockKey were synchronous and fire-and-forgot the async startKey promise. create_new_key.ts:35 already awaited loadNsec, but the await was a no-op against a sync return. Promoted both to async and properly awaited startKey, so backend.start() at least gets a chance to run before the caller's response goes out. Layer 2 — backend/index.ts: NDKNip46Backend.start() registers the kind-24133 subscription via this.ndk.subscribe(...) but returns immediately, before the relay's EOSE confirms it has the subscription on file. Override start() in our Backend subclass to await EOSE before resolving. This is the actual race-closer — layer 1's await alone wasn't enough because start() was still returning before the relay registered the subscription. Surfaced by aiolabs/lnbits#33's eager-bind chain, which publishes a NIP-46 connect event in the same HTTP round-trip as create_new_key. Pre-fix lnbits deferred the connect to first sign_event (minutes-to-hours after provisioning), so the race window was hidden. Verified end-to-end on bohm regtest: demo account creation through the webapp now completes cleanly, with bunker logs showing connect + sign_event for the freshly-provisioned key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit
65a6966b9f
|
|
@ -22,6 +22,43 @@ export class Backend extends NDKNip46Backend {
|
|||
// this.setStrategy('publish_event', new PublishEventHandlingStrategy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Override NDKNip46Backend.start() to await the kind-24133
|
||||
* subscription's EOSE before resolving. The base implementation
|
||||
* calls `this.ndk.subscribe(...)` and returns immediately — the
|
||||
* NDKSubscription queues a REQ on the relay connection but the
|
||||
* relay's acknowledgement (EOSE) hasn't arrived yet. Any caller
|
||||
* that publishes a NIP-46 event in the immediate window after
|
||||
* `start()` returns races against the relay registering this
|
||||
* subscription.
|
||||
*
|
||||
* aiolabs/lnbits#33's eager-bind chain publishes a NIP-46
|
||||
* `connect` event in the same HTTP round-trip as `create_new_key`,
|
||||
* which loses this race deterministically — the bunker never
|
||||
* sees the connect event because its subscription wasn't yet
|
||||
* registered with the relay when the event was broadcast.
|
||||
*
|
||||
* Awaiting EOSE closes the race: by the time `start()` resolves,
|
||||
* the relay has confirmed it has the bunker's subscription on
|
||||
* file and will route matching kind-24133 events to it.
|
||||
*
|
||||
* See aiolabs/nsecbunkerd#9 for the full diagnosis.
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
this.localUser = await this.signer.user();
|
||||
await new Promise<void>((resolve) => {
|
||||
const sub = this.ndk.subscribe(
|
||||
{
|
||||
kinds: [24133],
|
||||
"#p": [this.localUser!.pubkey],
|
||||
},
|
||||
{ closeOnEose: false }
|
||||
);
|
||||
sub.on("event", (e: any) => this.handleIncomingEvent(e));
|
||||
sub.on("eose", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
private async validateToken(token: string) {
|
||||
if (!token) throw new Error("Invalid token");
|
||||
|
||||
|
|
|
|||
|
|
@ -257,14 +257,14 @@ class Daemon {
|
|||
const nsec = decryptNsec(iv, data, passphrase);
|
||||
this.activeKeys[keyName] = nsec;
|
||||
|
||||
this.startKey(keyName, nsec);
|
||||
await this.startKey(keyName, nsec);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadNsec(keyName: string, nsec: string) {
|
||||
async loadNsec(keyName: string, nsec: string) {
|
||||
this.activeKeys[keyName] = nsec;
|
||||
|
||||
this.startKey(keyName, nsec);
|
||||
await this.startKey(keyName, nsec);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue