fix(transport): harden RelayPool — connect timeout, stop-race, cross-relay dedup
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
Folds three medium findings from the transport review into the RelayPool:
- RP-4: connectOnce used the static Relay.connect(), which silently DROPS a
{timeout} option in nostr-tools 2.20.0, so a black-holed TCP connect (SYN
accepted, never upgraded) stalled that relay's loop for the OS socket timeout
(minutes) with no retry. Now constructs the Relay and calls the instance
.connect({ timeout: 5000 }), which honours the timeout → prompt reject + backoff.
- RP-2: connectOnce didn't re-check `stopped` after the await. If stop() ran
while a connect was in flight, the resolved socket re-armed subscriptions on a
relay we meant to abandon, leaked the socket, and hung connectLoop (its promise
never resolved because onclose never fired). Now drops the socket cleanly and
resolves if stopped mid-connect.
- RP-1: no cross-relay event dedup — a kind:24133 request published to N relays
(the normal NIP-46 pattern), or re-delivered after a reconnect, drove the
daemon handler + recordSigning N times, making rate caps bind ~N× tighter
(fails closed, not open). Added a bounded (4000-id, ≈LRU) pool-wide seen-set;
onevent fires at most once per event id. Closes the CS-4 replay vector too.
Test: tests/relay-pool.test.ts asserts a duplicate event id is delivered once.
relay 3 / nip46 1 / admin 2 green; daemon bundles; tsc at baseline.
Refs: transport review RP-1/RP-2/RP-4, CS-4; #42
This commit is contained in:
parent
1754027249
commit
1c16a2a4b7
2 changed files with 88 additions and 5 deletions
|
|
@ -102,3 +102,35 @@ test("RelayPool.healthy() is false until the registry is subscribed on the wire
|
|||
pool.stop();
|
||||
await relay.stop();
|
||||
});
|
||||
|
||||
/**
|
||||
* RP-1: a NIP-46 request published to multiple relays (or re-delivered after a
|
||||
* reconnect) must drive the subscription callback ONCE — otherwise the daemon
|
||||
* signs N times and over-counts rate caps. The pool dedups by event id.
|
||||
*/
|
||||
test("RelayPool delivers each event id at most once (#RP-1)", async () => {
|
||||
const relay = new MockRelay();
|
||||
await relay.start();
|
||||
|
||||
const received: string[] = [];
|
||||
const pool = new RelayPool([relay.url], { log: () => {} });
|
||||
pool.start();
|
||||
await pool.subscribeAwaitingEose([{ kinds: [24133], "#p": [PUBKEY] }], (e) =>
|
||||
received.push(e.id),
|
||||
);
|
||||
|
||||
const ev = makeEvent();
|
||||
relay.inject(ev);
|
||||
relay.inject(ev); // same id again (simulates a second relay / a replay)
|
||||
await waitFor(() => received.includes(ev.id));
|
||||
await new Promise((r) => setTimeout(r, 200)); // give a 2nd delivery a chance
|
||||
|
||||
assert.equal(
|
||||
received.filter((id) => id === ev.id).length,
|
||||
1,
|
||||
"a duplicate event id is delivered to the callback only once",
|
||||
);
|
||||
|
||||
pool.stop();
|
||||
await relay.stop();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue