@serfab/cadre-core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +3 -2
  2. package/dist/announce-addrs.d.ts +49 -0
  3. package/dist/announce-addrs.js +78 -0
  4. package/dist/announce-addrs.js.map +1 -0
  5. package/dist/cadre-node.d.ts +607 -53
  6. package/dist/cadre-node.js +1268 -157
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-database.d.ts +174 -23
  9. package/dist/control-database.js +317 -107
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-read-retry.d.ts +103 -0
  12. package/dist/control-read-retry.js +145 -0
  13. package/dist/control-read-retry.js.map +1 -0
  14. package/dist/control-retry.d.ts +101 -0
  15. package/dist/control-retry.js +108 -0
  16. package/dist/control-retry.js.map +1 -0
  17. package/dist/control-schema.d.ts +1 -1
  18. package/dist/control-schema.js +50 -6
  19. package/dist/control-schema.js.map +1 -1
  20. package/dist/control-write-retry.d.ts +59 -35
  21. package/dist/control-write-retry.js +21 -92
  22. package/dist/control-write-retry.js.map +1 -1
  23. package/dist/delegate-admission.d.ts +17 -15
  24. package/dist/delegate-admission.js +17 -15
  25. package/dist/delegate-admission.js.map +1 -1
  26. package/dist/enrolled-machine-store-file.d.ts +13 -0
  27. package/dist/enrolled-machine-store-file.js +41 -0
  28. package/dist/enrolled-machine-store-file.js.map +1 -0
  29. package/dist/enrolled-machine-store.d.ts +81 -0
  30. package/dist/enrolled-machine-store.js +225 -0
  31. package/dist/enrolled-machine-store.js.map +1 -0
  32. package/dist/file-durable-slot.js +5 -3
  33. package/dist/file-durable-slot.js.map +1 -1
  34. package/dist/index.d.ts +10 -6
  35. package/dist/index.js +39 -12
  36. package/dist/index.js.map +1 -1
  37. package/dist/membership-connection-gater.d.ts +226 -26
  38. package/dist/membership-connection-gater.js +316 -30
  39. package/dist/membership-connection-gater.js.map +1 -1
  40. package/dist/node-local-snapshot.d.ts +6 -0
  41. package/dist/node-local-snapshot.js +7 -1
  42. package/dist/node-local-snapshot.js.map +1 -1
  43. package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
  44. package/dist/{strand-backfill.js → peer-join-backfill.js} +146 -56
  45. package/dist/peer-join-backfill.js.map +1 -0
  46. package/dist/peer-record.d.ts +26 -0
  47. package/dist/peer-record.js +32 -0
  48. package/dist/peer-record.js.map +1 -1
  49. package/dist/relay-addrs.d.ts +145 -22
  50. package/dist/relay-addrs.js +314 -31
  51. package/dist/relay-addrs.js.map +1 -1
  52. package/dist/relay-reservation.d.ts +20 -15
  53. package/dist/relay-reservation.js +18 -14
  54. package/dist/relay-reservation.js.map +1 -1
  55. package/dist/strand-addr-protocol.js +16 -0
  56. package/dist/strand-addr-protocol.js.map +1 -1
  57. package/dist/strand-database.d.ts +12 -0
  58. package/dist/strand-database.js +16 -0
  59. package/dist/strand-database.js.map +1 -1
  60. package/dist/strand-formation-manager.d.ts +22 -9
  61. package/dist/strand-formation-manager.js +20 -12
  62. package/dist/strand-formation-manager.js.map +1 -1
  63. package/dist/strand-formation-protocol.d.ts +83 -9
  64. package/dist/strand-formation-protocol.js +86 -10
  65. package/dist/strand-formation-protocol.js.map +1 -1
  66. package/dist/strand-instance-manager.d.ts +151 -17
  67. package/dist/strand-instance-manager.js +237 -32
  68. package/dist/strand-instance-manager.js.map +1 -1
  69. package/dist/strand-membership-writer.d.ts +118 -14
  70. package/dist/strand-membership-writer.js +203 -15
  71. package/dist/strand-membership-writer.js.map +1 -1
  72. package/dist/strand-network-config.d.ts +84 -0
  73. package/dist/strand-network-config.js +136 -0
  74. package/dist/strand-network-config.js.map +1 -0
  75. package/dist/strand-revocation-enforcer.d.ts +392 -0
  76. package/dist/strand-revocation-enforcer.js +533 -0
  77. package/dist/strand-revocation-enforcer.js.map +1 -0
  78. package/dist/strand-solicitation.d.ts +10 -3
  79. package/dist/strand-solicitation.js +12 -7
  80. package/dist/strand-solicitation.js.map +1 -1
  81. package/dist/strand-wake-protocol.d.ts +47 -2
  82. package/dist/strand-wake-protocol.js +76 -6
  83. package/dist/strand-wake-protocol.js.map +1 -1
  84. package/dist/strand-watcher.js +6 -1
  85. package/dist/strand-watcher.js.map +1 -1
  86. package/dist/types.d.ts +282 -43
  87. package/dist/types.js +7 -1
  88. package/dist/types.js.map +1 -1
  89. package/package.json +16 -8
  90. package/src/announce-addrs.ts +93 -0
  91. package/src/cadre-node.ts +1366 -171
  92. package/src/control-database.ts +2628 -2406
  93. package/src/control-read-retry.ts +167 -0
  94. package/src/control-retry.ts +190 -0
  95. package/src/control-schema.ts +50 -6
  96. package/src/control-write-retry.ts +30 -130
  97. package/src/delegate-admission.ts +17 -15
  98. package/src/enrolled-machine-store-file.ts +43 -0
  99. package/src/enrolled-machine-store.ts +280 -0
  100. package/src/file-durable-slot.ts +5 -3
  101. package/src/index.ts +477 -407
  102. package/src/membership-connection-gater.ts +539 -204
  103. package/src/node-local-snapshot.ts +13 -1
  104. package/src/peer-addr-book.ts +251 -250
  105. package/src/{strand-backfill.ts → peer-join-backfill.ts} +187 -75
  106. package/src/peer-record.ts +33 -0
  107. package/src/relay-addrs.ts +436 -111
  108. package/src/relay-reservation.ts +828 -823
  109. package/src/strand-addr-protocol.ts +17 -0
  110. package/src/strand-database.ts +17 -0
  111. package/src/strand-formation-manager.ts +511 -491
  112. package/src/strand-formation-protocol.ts +808 -692
  113. package/src/strand-instance-manager.ts +899 -611
  114. package/src/strand-membership-writer.ts +232 -15
  115. package/src/strand-network-config.ts +172 -0
  116. package/src/strand-revocation-enforcer.ts +653 -0
  117. package/src/strand-solicitation.ts +484 -472
  118. package/src/strand-wake-protocol.ts +94 -8
  119. package/src/strand-watcher.ts +319 -314
  120. package/src/types.ts +1554 -1309
  121. package/dist/strand-backfill.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Control-network inbound connection gate (defense-in-depth).
2
+ * Control-network inbound admission gates (defense-in-depth): the
3
+ * encrypted-connection checkpoint and the circuit-relay reservation checkpoint.
3
4
  *
4
5
  * Layer 2 of the membership enforcement chain: the PRIMARY gates are per-stream
5
6
  * — every sensitive sereus control protocol rejects a peer that fails the
@@ -10,7 +11,9 @@
10
11
  * time. This module adds the opportunistic connection-level layer on top: a
11
12
  * peer this node can positively determine is NOT authorized is refused at the
12
13
  * encrypted-connection checkpoint, before any protocol negotiation, so a
13
- * known-nothing outsider is never even in the conversation.
14
+ * known-nothing outsider is never even in the conversation — EXCEPT on a node
15
+ * that runs the circuit-relay server, where the refusal moves to the
16
+ * relay-reservation checkpoint instead (see "The relay-reservation seam").
14
17
  *
15
18
  * ## The stranger allowlist (the ONE place it is defined)
16
19
  *
@@ -45,14 +48,54 @@
45
48
  *
46
49
  * - **Announced delegate peers** (`delegate-admission.ts`). A member's strand
47
50
  * node runs as a separate libp2p identity whose peerId no sibling can
48
- * recompute, so a relay-running control node would deny its circuit-relay
49
- * reservation. Before starting a strand node, a member's control node
51
+ * recompute. Before starting a strand node, a member's control node
50
52
  * announces that peerId over the already-authenticated strand-addr RPC, and
51
53
  * the receiver holds a short-lived grant for it
52
- * (`CadreNode.grantDelegateAdmission`). The grant admits the CONNECTION and
53
- * nothing else — a reservation needs no stream — and is deliberately
54
- * invisible to the per-stream gate below, so a delegate still gets refused
55
- * on every members-only protocol.
54
+ * (`CadreNode.grantDelegateAdmission`). The grant admits the CONNECTION,
55
+ * admits the peer's RESERVATION at the seam below (without spending the
56
+ * unauthorized budget), and nothing else — it is deliberately invisible to
57
+ * the per-stream gate, so a delegate still gets refused on every
58
+ * members-only protocol.
59
+ *
60
+ * ## The relay-reservation seam
61
+ *
62
+ * A circuit-relay reservation is established by the reserving peer DIALING the
63
+ * relay, so at the relay it is an inbound connection — and killing that
64
+ * connection kills the reservation. For a genuine member whose `CadrePeer` row
65
+ * has not yet replicated to this node, a connection-level deny here is NOT
66
+ * self-healing the way a data connection's is: an outbound reconcile dial
67
+ * re-establishes a data link, but no outbound dial can grant the REMOTE peer a
68
+ * reservation, and a relay-only peer has no address of its own to dial back —
69
+ * the reservation IS its address. Boot ordering makes the window ordinary (a
70
+ * node's circuit listener runs inside `libp2p.start()`, before its own row
71
+ * could have replicated anywhere), and a stalled replication makes it
72
+ * unbounded. So the relay would be answering a RELAY question ("may this peer
73
+ * use my forwarding capacity?") with a MEMBERSHIP answer, at a checkpoint where
74
+ * a wrong answer is unrecoverable.
75
+ *
76
+ * On a node whose relay server is enabled the two questions are separated:
77
+ *
78
+ * - The policy returns `'admit-for-relay'` instead of `'deny'`, and the
79
+ * connection is ADMITTED. The fail-closed per-stream gates still refuse such
80
+ * a peer every members-only protocol, so it gains identify/ping and the
81
+ * relay hop protocol, nothing else.
82
+ * - The reservation itself is decided at libp2p's
83
+ * `denyInboundRelayReservation` hook (the circuit-relay server consults it
84
+ * per RESERVE request): the policy admits members, delegates and configured
85
+ * infra outright, and admits peers it cannot place only within a bounded
86
+ * budget ({@link UnauthorizedReservationBudget}) — a member whose row is in
87
+ * flight always finds a slot under any sane cap, while outsiders cannot
88
+ * annex the party's relay capacity.
89
+ * - An `'admit-for-relay'` connection that is NOT reserving is dropped: it has
90
+ * {@link RELAY_ADMISSION_RESERVE_DEADLINE_MS} to get a reservation ADMITTED
91
+ * at that hook, after which the underlying connection is aborted. The
92
+ * guarantee above thus weakens on relay-enabled nodes from "never in the
93
+ * conversation" to "in it briefly, and can speak nothing".
94
+ *
95
+ * The deadline clears on reservation ADMISSION, not on the reservation's own
96
+ * success: this gate cannot observe the server-side `reserve()` outcome, so a
97
+ * peer whose admitted reservation is then refused for server capacity keeps its
98
+ * connection until either side closes it — bounded and mute, so harmless.
56
99
  *
57
100
  * Everything else a control node handles — the Optimystic control-DB protocols
58
101
  * (`/optimystic/control-<party>/{repo,cluster,sync,block-transfer}/…`), wake
@@ -70,12 +113,45 @@
70
113
  * stranger carve-outs — an enrollment window admits a stranger's connection
71
114
  * for seed delivery, yet its repo streams are still refused.
72
115
  *
116
+ * ## The bring-up quiet period
117
+ *
118
+ * One state is decided BEFORE any of the above, and is not about the remote peer
119
+ * at all: while this node's control-database bring-up is in flight
120
+ * (`InboundAdmissionPolicy.bringUpInFlight`), the gate denies BOTH directions —
121
+ * `denyDialPeer` and `denyInboundEncryptedConnection` — and then opens.
122
+ *
123
+ * The invariant it protects is "`ControlDatabase.initialize()` runs while this
124
+ * node holds ZERO control connections". Building that database is a long chain
125
+ * of cohort-consulting block probes, and every connected same-party sibling is in
126
+ * the cohort those probes consult. A sibling that has not yet replicated this
127
+ * node's `CadrePeer` row correctly refuses them at its own fail-closed per-stream
128
+ * gate, so ONE connection in this window turns bring-up into
129
+ * `BlockUnavailableError` and `start()` rejects. Retrying cannot converge: the
130
+ * condition that would clear the refusal is this node's own row reaching the
131
+ * sibling, and writing that row needs the database the retry is building. So the
132
+ * only fix is to not be in the conversation yet — which is what this window is.
133
+ *
134
+ * The ordering is arranged so nothing SHOULD open a connection here anyway:
135
+ * `network.relayAddrs` resolves to a listener that dials nothing and reserves
136
+ * after bring-up (`relay-addrs.ts`), and the control-cohort reconcile pass is
137
+ * scheduled post-start. This window is what makes that a property rather than an
138
+ * accident of ordering — the live case it actually catches is
139
+ * `controlNetwork.bootstrapNodes`, where `@libp2p/bootstrap` emits its discovery
140
+ * events ~1 s after `libp2p.start()` and the connection manager auto-dials from
141
+ * there, which is a race bring-up wins only while raw-storage latency stays low.
142
+ *
143
+ * A denial here costs a retry, not a partition: libp2p's connection manager
144
+ * re-dials on its auto-dial cadence, the reservation supervisor re-drives, and a
145
+ * denied inbound peer reconnects. The window opens on bring-up FAILURE too (via
146
+ * `CadreNode.cleanup`), so teardown is never gated.
147
+ *
73
148
  * ## Fail-open, deliberately
74
149
  *
75
- * This layer only ever denies on a POSITIVE determination of "unauthorized
76
- * outsider while no stranger path is open". Any error, missing dependency, or
77
- * ambiguous state admits the connection and defers to the fail-closed stream
78
- * gates — a DB hiccup must not partition a legitimate cadre.
150
+ * Outside that window this layer only ever denies on a POSITIVE determination of
151
+ * "unauthorized outsider while no stranger path is open". Any error, missing
152
+ * dependency, or ambiguous state admits the connection (and the reservation) and
153
+ * defers to the fail-closed stream gates — a DB hiccup must not partition a
154
+ * legitimate cadre.
79
155
  */
80
156
  import debug from 'debug';
81
157
  import { SEED_PROTOCOL } from './seed-bootstrap.js';
@@ -97,7 +173,8 @@ export const STRANGER_OPEN_PROTOCOLS = [SEED_PROTOCOL, FORMATION_PROTOCOL];
97
173
  */
98
174
  export const DEFAULT_ENROLLMENT_WINDOW_MS = 30 * 60 * 1000;
99
175
  /**
100
- * Deadline for one admission decision, after which the connection is ADMITTED.
176
+ * Deadline for one admission decision, after which the fail-open outcome is
177
+ * used (connection admitted / reservation admitted).
101
178
  *
102
179
  * libp2p awaits `denyInboundEncryptedConnection` inside the inbound upgrade
103
180
  * WITHOUT racing its inbound-upgrade timeout signal (unlike the pre-encryption
@@ -110,16 +187,118 @@ export const DEFAULT_ENROLLMENT_WINDOW_MS = 30 * 60 * 1000;
110
187
  * failing closed (or not at all).
111
188
  */
112
189
  export const ADMISSION_DECISION_TIMEOUT_MS = 2000;
190
+ /**
191
+ * How long an `'admit-for-relay'` connection may exist without a relay
192
+ * reservation being ADMITTED at the `denyInboundRelayReservation` hook, after
193
+ * which the gate aborts the underlying connection. A reserving client asks for
194
+ * its slot immediately after the connection upgrades (`relay-reservation.ts`
195
+ * dials and requests in one drive; a strand node's configured circuit listener
196
+ * does the same from inside its own `listen()`), so a connection idle past this
197
+ * deadline is not reserving.
198
+ */
199
+ export const RELAY_ADMISSION_RESERVE_DEADLINE_MS = 5000;
200
+ /**
201
+ * Default cap on concurrent relay reservations held by peers the membership
202
+ * check could not place (see {@link UnauthorizedReservationBudget}). Small on
203
+ * purpose: it exists for the handful of genuine members whose rows are still
204
+ * in flight, not as public relay capacity. Overridable per node via
205
+ * `network.unauthorizedRelayReservationCap` (0 refuses every unauthorized
206
+ * reservation — the strict pre-seam posture).
207
+ *
208
+ * NOTE: this cap shares the relay server's own reservation store, whose default
209
+ * size is 15 (`@libp2p/circuit-relay-v2`'s `DEFAULT_MAX_RESERVATION_STORE_SIZE`);
210
+ * unplaced peers may therefore occupy up to this many of those slots, and the
211
+ * rest are what members and delegates compete for. This module cannot read the
212
+ * server's size, so the two are kept apart by hand — if either is raised, keep
213
+ * this one well under the store's, or a fleet of unplaceable peers can crowd
214
+ * genuine members out of the server's own store (which the gate cannot override).
215
+ */
216
+ export const MAX_UNAUTHORIZED_RELAY_RESERVATIONS = 8;
217
+ /**
218
+ * How long one {@link UnauthorizedReservationBudget} entry occupies a slot
219
+ * without a refresh. Mirrors the relay server's own default reservation TTL
220
+ * (`@libp2p/circuit-relay-v2`'s `DEFAULT_MAX_RESERVATION_TTL`, 2 h): the server
221
+ * holds an unrefreshed reservation exactly that long, and a live reserver
222
+ * re-requests (re-hitting the admission hook, refreshing its entry) well before
223
+ * expiry — so the live entry count tracks the server's own occupancy without
224
+ * this module reaching into the server's reservation store.
225
+ *
226
+ * NOTE: a mirror, not a live coupling — if the relay server is ever configured
227
+ * with a non-default `reservationTtl` (db-p2p's `relayServerInit`), update this
228
+ * constant alongside it or the budget's occupancy drifts from the server's.
229
+ */
230
+ export const UNAUTHORIZED_RESERVATION_TTL_MS = 2 * 60 * 60 * 1000;
231
+ /**
232
+ * Bounded budget of concurrent relay reservations for peers the membership
233
+ * check could not place. `tryAdmit` is the whole surface: an already-admitted
234
+ * peer refreshes its entry (a reservation refresh never double-counts), a new
235
+ * peer takes a free slot or is refused. Entries expire after `ttlMs` (see
236
+ * {@link UNAUTHORIZED_RESERVATION_TTL_MS} for why that mirrors the relay
237
+ * server's own hold). Injectable `now` keeps expiry testable without fake
238
+ * timers. Authorized members and delegates are never run through this — the
239
+ * policy admits them before consulting the budget, and `release`s the slot such
240
+ * a peer took while it was still unplaceable, so the boot-ordering window a
241
+ * member passes through costs the budget nothing once its row lands.
242
+ */
243
+ export class UnauthorizedReservationBudget {
244
+ constructor(cap = MAX_UNAUTHORIZED_RELAY_RESERVATIONS, ttlMs = UNAUTHORIZED_RESERVATION_TTL_MS) {
245
+ this.cap = cap;
246
+ this.ttlMs = ttlMs;
247
+ this.admitted = new Map();
248
+ }
249
+ /** Number of live (unexpired at last prune) entries — test/diagnostic surface. */
250
+ get size() {
251
+ return this.admitted.size;
252
+ }
253
+ /** Admit (or refresh) `remotePeerId` if a slot is free; false when the cap is spent. */
254
+ tryAdmit(remotePeerId, now = Date.now()) {
255
+ this.prune(now);
256
+ if (!this.admitted.has(remotePeerId) && this.admitted.size >= this.cap) {
257
+ log('Unauthorized-reservation budget spent (%d/%d) — refusing %s', this.admitted.size, this.cap, remotePeerId);
258
+ return false;
259
+ }
260
+ this.admitted.set(remotePeerId, now + this.ttlMs);
261
+ return true;
262
+ }
263
+ /**
264
+ * Give back the slot `remotePeerId` holds, if any — called when the peer has
265
+ * become admissible on its own merits (its membership row replicated, or a
266
+ * delegate grant landed), so the slot it took during the boot-ordering window
267
+ * does not stay spent for the remaining TTL on a peer that no longer needs it.
268
+ */
269
+ release(remotePeerId) {
270
+ this.admitted.delete(remotePeerId);
271
+ }
272
+ prune(now) {
273
+ for (const [peerId, expiresAt] of this.admitted) {
274
+ if (expiresAt <= now) {
275
+ this.admitted.delete(peerId);
276
+ }
277
+ }
278
+ }
279
+ }
113
280
  /**
114
281
  * Build the control node's connection gater: the caller-supplied gater (if any)
115
282
  * with membership admission composed onto `denyInboundEncryptedConnection` —
116
- * the earliest checkpoint where the remote's authenticated PeerId is known.
283
+ * the earliest checkpoint where the remote's authenticated PeerId is known —
284
+ * and reservation admission composed onto `denyInboundRelayReservation` — the
285
+ * hook the circuit-relay server consults per RESERVE request (inert on a node
286
+ * whose relay server is off; libp2p never calls it there).
287
+ *
288
+ * `denyDialPeer` is composed too, but ONLY for the bring-up quiet period (see
289
+ * the module doc): outside that window outbound dials are never gated by
290
+ * membership — this node decides for itself who to talk to.
291
+ *
292
+ * Composition semantics: every hook of `base` is preserved as-is; on the three
293
+ * composed hooks a deny from EITHER the base gater or the admission policy
294
+ * denies. A policy error — or a decision slower than `decisionTimeoutMs` —
295
+ * takes the fail-open outcome (connection admitted / reservation admitted, see
296
+ * module doc); the base gater's verdict is still honored first.
117
297
  *
118
- * Composition semantics: every hook of `base` is preserved as-is; on the
119
- * inbound-encrypted hook a deny from EITHER the base gater or the admission
120
- * policy denies. A policy error — or a decision slower than
121
- * `decisionTimeoutMs` — admits (fail-open, see module doc); the base gater's
122
- * verdict is still honored first.
298
+ * An `'admit-for-relay'` verdict admits the connection and arms a
299
+ * `reserveDeadlineMs` timer against it; the timer is disarmed when a
300
+ * reservation for that peer is ADMITTED at the reservation hook, and aborts the
301
+ * underlying `MultiaddrConnection` when it fires first.
123
302
  *
124
303
  * NOTE: `base` is spread, so a gater passed as a CLASS INSTANCE would lose its
125
304
  * prototype methods; every caller in this repo (and libp2p's own default)
@@ -134,41 +313,88 @@ export const ADMISSION_DECISION_TIMEOUT_MS = 2000;
134
313
  * dialer sees its "open" connection close moments later.
135
314
  *
136
315
  * Control node only: strand cohort nodes legitimately connect cross-party
137
- * peers, so `CadreNode` threads the raw configured gater to them unchanged.
316
+ * peers, so cadre membership never gates them. An OPEN strand's node receives
317
+ * the raw configured gater unchanged; a CLOSED strand's node composes its own
318
+ * revoked-peer denial onto it instead — a deny-list on positive revocation
319
+ * evidence, not this module's membership admission — see
320
+ * `strand-revocation-enforcer.ts`.
138
321
  */
139
- export function createMembershipConnectionGater(policy, base, decisionTimeoutMs = ADMISSION_DECISION_TIMEOUT_MS) {
322
+ export function createMembershipConnectionGater(policy, base, decisionTimeoutMs = ADMISSION_DECISION_TIMEOUT_MS, reserveDeadlineMs = RELAY_ADMISSION_RESERVE_DEADLINE_MS) {
323
+ const pendingReservations = new PendingReserveDeadlines(reserveDeadlineMs);
324
+ const quiet = () => policy.bringUpInFlight?.() ?? false;
140
325
  return {
141
326
  ...base,
327
+ denyDialPeer: async (peerId) => {
328
+ if (await base?.denyDialPeer?.(peerId)) {
329
+ return true;
330
+ }
331
+ if (quiet()) {
332
+ log('Control-database bring-up in flight — refusing the outbound dial to %s', peerId.toString());
333
+ return true;
334
+ }
335
+ return false;
336
+ },
142
337
  denyInboundEncryptedConnection: async (peerId, maConn) => {
143
338
  if (await base?.denyInboundEncryptedConnection?.(peerId, maConn)) {
144
339
  return true;
145
340
  }
341
+ if (quiet()) {
342
+ log('Control-database bring-up in flight — refusing the inbound connection from %s', peerId.toString());
343
+ return true;
344
+ }
146
345
  const remotePeerId = peerId.toString();
346
+ let verdict;
147
347
  try {
148
- return !(await admitWithinDeadline(policy, remotePeerId, decisionTimeoutMs));
348
+ verdict = await decideWithinDeadline(() => policy.admitInbound(remotePeerId), 'admit', decisionTimeoutMs, `admitInbound(${remotePeerId})`);
149
349
  }
150
350
  catch (error) {
151
351
  log('admitInbound threw for %s — admitting (fail-open; stream gates decide): %o', remotePeerId, error);
152
352
  return false;
153
353
  }
354
+ if (verdict === 'admit-for-relay') {
355
+ pendingReservations.arm(remotePeerId, maConn);
356
+ return false;
357
+ }
358
+ return verdict === 'deny';
359
+ },
360
+ denyInboundRelayReservation: async (peerId) => {
361
+ if (await base?.denyInboundRelayReservation?.(peerId)) {
362
+ return true;
363
+ }
364
+ const remotePeerId = peerId.toString();
365
+ let admitted;
366
+ try {
367
+ admitted = await decideWithinDeadline(() => policy.admitRelayReservation(remotePeerId), true, decisionTimeoutMs, `admitRelayReservation(${remotePeerId})`);
368
+ }
369
+ catch (error) {
370
+ log('admitRelayReservation threw for %s — admitting (fail-open): %o', remotePeerId, error);
371
+ admitted = true;
372
+ }
373
+ if (admitted) {
374
+ pendingReservations.disarm(remotePeerId);
375
+ }
376
+ return !admitted;
154
377
  }
155
378
  };
156
379
  }
157
380
  /**
158
- * Run the admission decision under {@link ADMISSION_DECISION_TIMEOUT_MS},
159
- * resolving to "admit" if it has not settled in time (see that constant for why
160
- * an unbounded await is not safe here). The timer is always cleared so a decided
161
- * connection never holds the event loop open.
381
+ * Run one admission decision under `timeoutMs`, resolving to `fallback` (the
382
+ * fail-open outcome) if it has not settled in time — see
383
+ * {@link ADMISSION_DECISION_TIMEOUT_MS} for why an unbounded await is not safe
384
+ * on the connection hook. The timer is always cleared so a decided call never
385
+ * holds the event loop open. Exported for the strand revoked-peer gate
386
+ * (`strand-revocation-enforcer.ts`), which bounds its connection hooks with the
387
+ * same fail-open contract.
162
388
  */
163
- async function admitWithinDeadline(policy, remotePeerId, timeoutMs) {
389
+ export async function decideWithinDeadline(decide, fallback, timeoutMs, what) {
164
390
  let timer;
165
391
  try {
166
392
  return await Promise.race([
167
- Promise.resolve(policy.admitInbound(remotePeerId)),
393
+ Promise.resolve().then(decide),
168
394
  new Promise((resolve) => {
169
395
  timer = setTimeout(() => {
170
- log('admitInbound exceeded %dms for %s — admitting (fail-open; stream gates decide)', timeoutMs, remotePeerId);
171
- resolve(true);
396
+ log('%s exceeded %dms — taking the fail-open outcome (stream gates decide)', what, timeoutMs);
397
+ resolve(fallback);
172
398
  }, timeoutMs);
173
399
  })
174
400
  ]);
@@ -177,4 +403,64 @@ async function admitWithinDeadline(policy, remotePeerId, timeoutMs) {
177
403
  clearTimeout(timer);
178
404
  }
179
405
  }
406
+ /**
407
+ * The not-reserving deadlines for `'admit-for-relay'` connections, keyed by
408
+ * remote peerId (a Set per peer — one peer can hold several in-flight
409
+ * connections). `arm` starts a timer that aborts the connection; `disarm`
410
+ * (called when a reservation for that peer is admitted) cancels every pending
411
+ * timer for the peer. Timers are unref'd so an armed deadline never holds a
412
+ * process open, and a timer that fires against an already-closed connection is
413
+ * a swallowed no-op.
414
+ *
415
+ * Disarming is final for the connections it cancelled: a peer that reserved
416
+ * once and then lets its reservation lapse keeps a mute connection (the
417
+ * fail-closed stream gates refuse it every members-only protocol) until either
418
+ * side closes it. Bounded — only a peer the reservation policy already admitted
419
+ * can reach that state, so unplaceable peers are bounded by the budget cap.
420
+ */
421
+ class PendingReserveDeadlines {
422
+ constructor(deadlineMs) {
423
+ this.deadlineMs = deadlineMs;
424
+ this.byPeer = new Map();
425
+ }
426
+ arm(remotePeerId, maConn) {
427
+ const entry = {
428
+ maConn,
429
+ timer: setTimeout(() => this.expire(remotePeerId, entry), this.deadlineMs)
430
+ };
431
+ entry.timer.unref?.();
432
+ let entries = this.byPeer.get(remotePeerId);
433
+ if (!entries) {
434
+ entries = new Set();
435
+ this.byPeer.set(remotePeerId, entries);
436
+ }
437
+ entries.add(entry);
438
+ log('Admitted %s for relay only — dropping the connection unless a reservation is admitted within %dms', remotePeerId, this.deadlineMs);
439
+ }
440
+ disarm(remotePeerId) {
441
+ const entries = this.byPeer.get(remotePeerId);
442
+ if (!entries) {
443
+ return;
444
+ }
445
+ this.byPeer.delete(remotePeerId);
446
+ for (const entry of entries) {
447
+ clearTimeout(entry.timer);
448
+ }
449
+ }
450
+ expire(remotePeerId, entry) {
451
+ const entries = this.byPeer.get(remotePeerId);
452
+ entries?.delete(entry);
453
+ if (entries?.size === 0) {
454
+ this.byPeer.delete(remotePeerId);
455
+ }
456
+ log('Relay-only admission expired for %s — no reservation admitted within %dms, aborting the connection', remotePeerId, this.deadlineMs);
457
+ try {
458
+ entry.maConn.abort(new Error(`relay-only admission expired: no relay reservation admitted within ${this.deadlineMs}ms`));
459
+ }
460
+ catch (error) {
461
+ // Aborting a connection that already closed on its own is not a failure.
462
+ log('Aborting the expired relay-only connection from %s threw: %o', remotePeerId, error);
463
+ }
464
+ }
465
+ }
180
466
  //# sourceMappingURL=membership-connection-gater.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"membership-connection-gater.js","sourceRoot":"","sources":["../src/membership-connection-gater.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,MAAM,GAAG,GAAG,KAAK,CAAC,+BAA+B,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAsB,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;AAE9F;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAK,CAAC;AAanD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,+BAA+B,CAC7C,MAA8B,EAC9B,IAAsB,EACtB,oBAA4B,6BAA6B;IAEzD,OAAO;QACL,GAAG,IAAI;QACP,8BAA8B,EAAE,KAAK,EAAE,MAAc,EAAE,MAA2B,EAAoB,EAAE;YACtG,IAAI,MAAM,IAAI,EAAE,8BAA8B,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;gBACjE,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,OAAO,CAAC,CAAC,MAAM,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,iBAAiB,CAAC,CAAC,CAAC;YAC/E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,GAAG,CAAC,4EAA4E,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC;gBACvG,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,mBAAmB,CAChC,MAA8B,EAC9B,YAAoB,EACpB,SAAiB;IAEjB,IAAI,KAAgD,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YAClD,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;gBAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACtB,GAAG,CAAC,gFAAgF,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;oBAC/G,OAAO,CAAC,IAAI,CAAC,CAAC;gBAChB,CAAC,EAAE,SAAS,CAAC,CAAC;YAChB,CAAC,CAAC;SACH,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"membership-connection-gater.js","sourceRoot":"","sources":["../src/membership-connection-gater.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,12 @@ export interface DurableSlot {
12
12
  * failed read reported as `undefined` silently converts a recoverable error
13
13
  * (permissions, a blocked database upgrade, an I/O fault) into destruction
14
14
  * of a still-intact record on the next save.
15
+ *
16
+ * A slot always reports the fault; what a READER does with it is the
17
+ * reader's policy. The two records here rethrow it; the enrolled-machine
18
+ * count catches it and cold-starts, because losing a repair hint must not
19
+ * stop a node. So an implementation must never soften this to `undefined`
20
+ * on a reader's behalf.
15
21
  */
16
22
  load(): Promise<string | undefined>;
17
23
  /**
@@ -6,7 +6,13 @@
6
6
  * - the trusted-owner anchor (`trusted-owner-store.ts`),
7
7
  * - the cold-start bootstrap-peer store (`bootstrap-peer-store.ts`).
8
8
  *
9
- * Both are the same mechanism with a different payload: a
9
+ * A THIRD node-local record — the enrolled-machine count
10
+ * (`enrolled-machine-store.ts`) — reuses {@link DurableSlot} but deliberately
11
+ * NOT this module's snapshot machinery: it is one scalar rather than an entry
12
+ * map, and it must cold-start rather than throw on an unreadable slot. Its
13
+ * module comment carries the reasoning; do not fold it in here.
14
+ *
15
+ * Both records here are the same mechanism with a different payload: a
10
16
  * `{ version, partyId, <entries> }` envelope, snapshot-written whole on every
11
17
  * change, loaded under one fail-safe-but-not-fail-silent policy. The only part
12
18
  * that differs per platform is WHERE the bytes are kept — Node writes a file,
@@ -1 +1 @@
1
- {"version":3,"file":"node-local-snapshot.js","sourceRoot":"","sources":["../src/node-local-snapshot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-local-snapshot.js","sourceRoot":"","sources":["../src/node-local-snapshot.ts"],"names":[],"mappings":"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"}
@@ -8,8 +8,8 @@ import { BlockTransferClient, type IRawStorage } from '@optimystic/db-p2p';
8
8
  * changes it or starts exporting it.
9
9
  */
10
10
  export declare const MAX_BLOCK_MESSAGE_BYTES: number;
11
- /** Tuning for the per-peer strand catch-up. Every field optional; defaults in {@link DEFAULT_STRAND_BACKFILL}. */
12
- export interface StrandBackfillConfig {
11
+ /** Tuning for the per-peer catch-up. Every field optional; defaults in {@link DEFAULT_PEER_JOIN_BACKFILL}. */
12
+ export interface PeerJoinBackfillConfig {
13
13
  /** Default true. False disables the catch-up entirely (the pre-existing behaviour). */
14
14
  enabled?: boolean;
15
15
  /** Settle time after a connection opens before catching that peer up, ms. Default 1000. */
@@ -25,32 +25,39 @@ export interface StrandBackfillConfig {
25
25
  /** Per-push response deadline, ms. Default 10_000 (matches SpreadOnChurnMonitor). */
26
26
  responseTimeoutMs?: number;
27
27
  }
28
- /** The resolved defaults every {@link StrandBackfill} starts from. */
29
- export declare const DEFAULT_STRAND_BACKFILL: Required<StrandBackfillConfig>;
28
+ /** The resolved defaults every {@link PeerJoinBackfill} starts from. */
29
+ export declare const DEFAULT_PEER_JOIN_BACKFILL: Required<PeerJoinBackfillConfig>;
30
30
  /**
31
31
  * The one capability the catch-up needs from a transfer client. Structural (rather than
32
32
  * the concrete `BlockTransferClient`) so unit tests can capture pushes without dialing
33
- * libp2p — see {@link StrandBackfillDeps.createPushClient}.
33
+ * libp2p — see {@link PeerJoinBackfillDeps.createPushClient}.
34
34
  */
35
- export type StrandBackfillPushClient = Pick<BlockTransferClient, 'pushBlocks'>;
36
- export interface StrandBackfillDeps {
37
- strandId: string;
38
- /** The STRAND's libp2p node (not the control node) — source of connection events and peer ids. */
35
+ export type PeerJoinBackfillPushClient = Pick<BlockTransferClient, 'pushBlocks'>;
36
+ export interface PeerJoinBackfillDeps {
37
+ /** Log tag naming which network this catch-up serves (a strand id, or `control-<partyId>`). */
38
+ label: string;
39
+ /** The libp2p node of the network being caught up — source of connection events and peer ids. */
39
40
  libp2p: Libp2p;
40
41
  /** `node.keyNetwork`, the IPeerNetwork BlockTransferClient dials through. */
41
42
  peerNetwork: IPeerNetwork;
42
- /** This strand's own raw block store — the same instance handed to the libp2p node. */
43
+ /** This network's own raw block store — the same instance handed to the libp2p node. */
43
44
  storage: IRawStorage;
44
- /** Must equal the prefix the receiver registered its handler under: `/optimystic/strand-<strandId>`. */
45
+ /** Must equal the prefix the receiver registered its handler under: `/optimystic/<networkName>`. */
45
46
  protocolPrefix: string;
47
+ /**
48
+ * Membership gate, consulted at PUSH time (fails closed on a throw). Absent = push to
49
+ * every connected peer — correct for strand networks, NEVER for the control network;
50
+ * see the module comment for why the strand no-gate argument does not carry over.
51
+ */
52
+ authorizePeer?: (peerId: string) => Promise<boolean>;
46
53
  /**
47
54
  * Test seam: build the per-peer push client. Defaults to a real
48
55
  * `BlockTransferClient` over {@link peerNetwork} + {@link protocolPrefix}.
49
56
  */
50
- createPushClient?: (peerId: PeerId) => StrandBackfillPushClient;
57
+ createPushClient?: (peerId: PeerId) => PeerJoinBackfillPushClient;
51
58
  }
52
59
  /** What one peer's catch-up actually did. Returned for tests and logged at the end of each run. */
53
- export interface StrandBackfillResult {
60
+ export interface PeerJoinBackfillResult {
54
61
  /** Blocks offered: had a committed `latest` AND materialized content locally. */
55
62
  offered: number;
56
63
  /** Blocks the remote reported it persisted. */
@@ -65,19 +72,21 @@ export interface StrandBackfillResult {
65
72
  capped: number;
66
73
  /** Skipped: a single block whose wire size alone exceeds {@link MAX_BLOCK_MESSAGE_BYTES}. */
67
74
  oversized: string[];
75
+ /** True when the `authorizePeer` gate refused (or threw) — nothing was pushed, peer not memoized. */
76
+ denied: boolean;
68
77
  }
69
78
  /**
70
- * Copies every block in one strand's own raw store to each peer its libp2p node connects
71
- * to, so a peer that joined the strand after blocks were committed still ends up holding
79
+ * Copies every block in one network's own raw store to each peer its libp2p node connects
80
+ * to, so a peer that joined the network after blocks were committed still ends up holding
72
81
  * them physically (the receiver persists each push via `saveReplicatedBlock`, which is
73
82
  * monotonic and idempotent — crossing pushes from both ends cannot regress a revision).
74
83
  *
75
- * Best-effort throughout: nothing here throws into a libp2p event handler or into
76
- * `buildStrandRuntime`; a failed chunk is logged and the run continues. A peer is marked
84
+ * Best-effort throughout: nothing here throws into a libp2p event handler or into a
85
+ * runtime bring-up; a failed chunk is logged and the run continues. A peer is marked
77
86
  * fully caught up ONLY after a run with no thrown chunk and an empty `missing` list, so
78
87
  * the next `connection:open` from a peer whose catch-up failed retries it.
79
88
  */
80
- export declare class StrandBackfill {
89
+ export declare class PeerJoinBackfill {
81
90
  private readonly deps;
82
91
  private readonly config;
83
92
  private readonly createPushClient;
@@ -87,15 +96,35 @@ export declare class StrandBackfill {
87
96
  private readonly done;
88
97
  /** Peers with a catch-up currently running (suppresses concurrent duplicates). */
89
98
  private readonly inFlight;
99
+ /**
100
+ * Peers whose (re-)schedule arrived while their OWN run was in flight, replayed once
101
+ * that run finishes. Load-bearing for the gated path: the gate's authorization check is
102
+ * a control-database read, so the window between it and the run's end is wide enough for
103
+ * the very membership commit that would have authorized the peer to land inside it —
104
+ * dropping that schedule (rather than deferring it) leaves the denied peer waiting for a
105
+ * reconnect, which is exactly what the re-arm exists to avoid.
106
+ */
107
+ private readonly rearmAfterFlight;
90
108
  /** Pending per-peer debounce timers, cleared on stop. */
91
109
  private readonly timers;
92
110
  private readonly onConnectionOpen;
93
111
  private loggedNoListBlockIds;
94
- constructor(deps: StrandBackfillDeps, config?: StrandBackfillConfig);
112
+ constructor(deps: PeerJoinBackfillDeps, config?: PeerJoinBackfillConfig);
95
113
  /** Subscribe to connection:open AND schedule a catch-up for peers already connected. */
96
114
  start(): void;
97
115
  /** Unsubscribe, clear timers; in-flight runs observe the stopped flag and bail. */
98
116
  stop(): void;
117
+ /**
118
+ * (Re-)schedule a debounced catch-up for every currently-connected peer not yet caught
119
+ * up. Idempotent and cheap (caught-up peers are skipped before any timer is set, and a
120
+ * peer whose run is in flight is deferred to the end of that run rather than dropped).
121
+ * Driven by {@link start}, and — on a gated network — by the embedder whenever
122
+ * membership changes, so a peer whose first pass was denied (connected before it was
123
+ * authorized: the production join order) is retried without waiting for a reconnect.
124
+ * Returns how many distinct peers were considered (for the start() log) — a peer holding
125
+ * several connections is one peer, and is scheduled once.
126
+ */
127
+ scheduleConnectedPeers(): number;
99
128
  /** Debounced entry point for connection churn: one run per peer per settle window. */
100
129
  private schedulePeer;
101
130
  /**
@@ -103,7 +132,7 @@ export declare class StrandBackfill {
103
132
  * all-zero result without pushing when the peer is already caught up, already being
104
133
  * caught up, or this backfill is stopped.
105
134
  */
106
- catchUpPeer(peerId: PeerId): Promise<StrandBackfillResult>;
135
+ catchUpPeer(peerId: PeerId): Promise<PeerJoinBackfillResult>;
107
136
  /** The actual copy. `clean` = every chunk pushed and the remote persisted every block. */
108
137
  private runCatchUp;
109
138
  }