@serfab/cadre-core 1.7.0 → 1.8.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.
- package/README.md +8 -6
- package/dist/cadre-node.d.ts +386 -114
- package/dist/cadre-node.js +887 -359
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +6 -0
- package/dist/control-cohort.js +6 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +46 -8
- package/dist/control-database.js +97 -18
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +37 -14
- package/dist/control-formation-recorder.js +60 -10
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +13 -12
- package/dist/control-read-retry.js +14 -12
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +67 -5
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +9 -1
- package/dist/control-stream.js +24 -1
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +12 -2
- package/dist/control-write-retry.js +13 -2
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +11 -8
- package/dist/delegate-admission.js +19 -11
- package/dist/delegate-admission.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +0 -7
- package/dist/diagnostics/connection-path.js +0 -7
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/formation-approval.d.ts +7 -0
- package/dist/formation-approval.js +9 -3
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +11 -2
- package/dist/fs-atomic.js +13 -3
- package/dist/fs-atomic.js.map +1 -1
- package/dist/hibernation-manager.d.ts +111 -8
- package/dist/hibernation-manager.js +234 -49
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +90 -25
- package/dist/joined-strand-store.js +223 -55
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +14 -5
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +367 -69
- package/dist/link-budget.js +375 -72
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +30 -43
- package/dist/membership-connection-gater.js +31 -42
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/peer-dial.d.ts +24 -4
- package/dist/peer-dial.js +74 -5
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +2 -1
- package/dist/peer-join-backfill.js +6 -5
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/push-fanout.d.ts +6 -0
- package/dist/push-fanout.js +6 -0
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-reservation.d.ts +3 -1
- package/dist/relay-reservation.js +3 -1
- package/dist/relay-reservation.js.map +1 -1
- package/dist/relay-server.d.ts +79 -0
- package/dist/relay-server.js +74 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +57 -4
- package/dist/seed-bootstrap.js +107 -83
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +33 -17
- package/dist/storage-scope.js +44 -23
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +58 -21
- package/dist/strand-addr-protocol.js +133 -85
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +20 -2
- package/dist/strand-database.js +42 -5
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +59 -21
- package/dist/strand-first-sync-gate.js +58 -19
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +151 -0
- package/dist/strand-formation-deadlines.js +171 -0
- package/dist/strand-formation-deadlines.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +63 -30
- package/dist/strand-formation-manager.js +67 -41
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +45 -35
- package/dist/strand-formation-protocol.js +59 -122
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +113 -6
- package/dist/strand-instance-manager.js +257 -41
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +6 -0
- package/dist/strand-membership-writer.js +11 -0
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-peer-book-protocol.d.ts +13 -4
- package/dist/strand-peer-book-protocol.js +3 -7
- package/dist/strand-peer-book-protocol.js.map +1 -1
- package/dist/strand-peer-book.d.ts +1 -1
- package/dist/strand-peer-book.js +8 -1
- package/dist/strand-peer-book.js.map +1 -1
- package/dist/strand-peer-observer.js +1 -1
- package/dist/strand-revocation-enforcer.js +2 -1
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +36 -20
- package/dist/strand-solicitation.js +4 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +80 -23
- package/dist/strand-wake-protocol.js +89 -64
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +128 -38
- package/dist/types.js +4 -3
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/cadre-node.ts +8367 -7776
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +6 -0
- package/src/control-database.ts +122 -18
- package/src/control-formation-recorder.ts +354 -305
- package/src/control-read-retry.ts +14 -12
- package/src/control-schema.ts +67 -5
- package/src/control-stream.ts +25 -1
- package/src/control-write-retry.ts +13 -2
- package/src/delegate-admission.ts +23 -13
- package/src/diagnostics/connection-path.ts +0 -7
- package/src/formation-approval.ts +610 -604
- package/src/fs-atomic.ts +14 -4
- package/src/hibernation-manager.ts +638 -437
- package/src/index.ts +28 -10
- package/src/joined-strand-store.ts +247 -57
- package/src/key-store-file.ts +14 -5
- package/src/link-budget.ts +393 -74
- package/src/membership-connection-gater.ts +31 -45
- package/src/peer-dial.ts +81 -7
- package/src/peer-join-backfill.ts +8 -6
- package/src/push-fanout.ts +6 -0
- package/src/relay-reservation.ts +3 -1
- package/src/relay-server.ts +98 -0
- package/src/seed-bootstrap.ts +133 -91
- package/src/storage-scope.ts +44 -23
- package/src/strand-addr-protocol.ts +547 -458
- package/src/strand-database.ts +287 -242
- package/src/strand-first-sync-gate.ts +58 -19
- package/src/strand-formation-deadlines.ts +227 -0
- package/src/strand-formation-manager.ts +700 -647
- package/src/strand-formation-protocol.ts +103 -150
- package/src/strand-instance-manager.ts +287 -44
- package/src/strand-membership-writer.ts +12 -0
- package/src/strand-peer-book-protocol.ts +16 -13
- package/src/strand-peer-book.ts +8 -1
- package/src/strand-peer-observer.ts +1 -1
- package/src/strand-revocation-enforcer.ts +2 -1
- package/src/strand-solicitation.ts +519 -500
- package/src/strand-wake-protocol.ts +401 -355
- package/src/types.ts +130 -38
|
@@ -85,7 +85,10 @@
|
|
|
85
85
|
* infra outright, and admits peers it cannot place only within a bounded
|
|
86
86
|
* budget ({@link UnauthorizedReservationBudget}) — a member whose row is in
|
|
87
87
|
* flight always finds a slot under any sane cap, while outsiders cannot
|
|
88
|
-
* annex the party's relay capacity.
|
|
88
|
+
* annex the party's relay capacity. COUNT and lifetime are the only bounds
|
|
89
|
+
* on such a peer: a party-run relay forwards without libp2p's per-connection
|
|
90
|
+
* data and duration limit by default (`relay-server.ts`), so what a granted
|
|
91
|
+
* slot carries is not capped.
|
|
89
92
|
* - An `'admit-for-relay'` connection that is NOT reserving is dropped: it has
|
|
90
93
|
* {@link RELAY_ADMISSION_RESERVE_DEADLINE_MS} to get a reservation ADMITTED
|
|
91
94
|
* at that hook, after which the gate CLOSES the underlying connection so
|
|
@@ -160,6 +163,8 @@ import type { ConnectionGater, PeerId, MultiaddrConnection } from '@libp2p/inter
|
|
|
160
163
|
import { SEED_PROTOCOL } from './seed-bootstrap.js';
|
|
161
164
|
import { FORMATION_PROTOCOL } from './strand-formation-protocol.js';
|
|
162
165
|
import { withDeadline } from './control-stream.js';
|
|
166
|
+
import { PARTY_RELAY_RESERVATION_TTL_MS } from './relay-server.js';
|
|
167
|
+
import { ADMISSION_DECISION_TIMEOUT_MS, relayAdmissionReserveDeadlineMs } from './link-budget.js';
|
|
163
168
|
|
|
164
169
|
const log = debug('sereus:cadre:connection-gater');
|
|
165
170
|
|
|
@@ -180,22 +185,6 @@ export const STRANGER_OPEN_PROTOCOLS: readonly string[] = [SEED_PROTOCOL, FORMAT
|
|
|
180
185
|
*/
|
|
181
186
|
export const DEFAULT_ENROLLMENT_WINDOW_MS = 30 * 60 * 1000;
|
|
182
187
|
|
|
183
|
-
/**
|
|
184
|
-
* Deadline for one admission decision, after which the fail-open outcome is
|
|
185
|
-
* used (connection admitted / reservation admitted).
|
|
186
|
-
*
|
|
187
|
-
* libp2p awaits `denyInboundEncryptedConnection` inside the inbound upgrade
|
|
188
|
-
* WITHOUT racing its inbound-upgrade timeout signal (unlike the pre-encryption
|
|
189
|
-
* `denyInboundConnection` hook), so a decision that never settles wedges that
|
|
190
|
-
* upgrade forever — the connection-manager's inbound-upgrade slot is taken by
|
|
191
|
-
* `acceptIncomingConnection` and released in the `finally` that never runs. The
|
|
192
|
-
* real policy reads the control DB (`listAuthorizedMembers`), which can pull
|
|
193
|
-
* over the network, so "never settles" is reachable. Bounding it here keeps the
|
|
194
|
-
* fail-open contract honest: a slow decision admits rather than silently
|
|
195
|
-
* failing closed (or not at all).
|
|
196
|
-
*/
|
|
197
|
-
export const ADMISSION_DECISION_TIMEOUT_MS = 2_000;
|
|
198
|
-
|
|
199
188
|
/**
|
|
200
189
|
* How long an `'admit-for-relay'` connection may exist without a relay
|
|
201
190
|
* reservation being ADMITTED at the `denyInboundRelayReservation` hook, after
|
|
@@ -204,8 +193,13 @@ export const ADMISSION_DECISION_TIMEOUT_MS = 2_000;
|
|
|
204
193
|
* dials and requests in one drive; a strand node's configured circuit listener
|
|
205
194
|
* does the same from inside its own `listen()`), so a connection idle past this
|
|
206
195
|
* deadline is not reserving.
|
|
196
|
+
*
|
|
197
|
+
* Derived from the link ({@link relayAdmissionReserveDeadlineMs}): one link round trip for the
|
|
198
|
+
* request to arrive plus one admission decision on it. This is the value at the default
|
|
199
|
+
* declaration; `CadreNode` passes the one derived from the relay's OWN declaration, because the
|
|
200
|
+
* relay is the machine that decides.
|
|
207
201
|
*/
|
|
208
|
-
export const RELAY_ADMISSION_RESERVE_DEADLINE_MS =
|
|
202
|
+
export const RELAY_ADMISSION_RESERVE_DEADLINE_MS = relayAdmissionReserveDeadlineMs();
|
|
209
203
|
|
|
210
204
|
/**
|
|
211
205
|
* Bound on the graceful close of an expired relay-only connection. It exists
|
|
@@ -218,6 +212,7 @@ export const RELAY_ADMISSION_RESERVE_DEADLINE_MS = 5_000;
|
|
|
218
212
|
* Applied through {@link withDeadline}, not `AbortSignal.timeout`: the latter is
|
|
219
213
|
* not reliably present on React Native/Hermes, which loads this same module.
|
|
220
214
|
*/
|
|
215
|
+
// eslint-disable-next-line no-restricted-syntax -- link-independent: bounds a local connection close inside a timer callback, and expiry aborts the connection instead
|
|
221
216
|
export const RELAY_ADMISSION_CLOSE_TIMEOUT_MS = 2_000;
|
|
222
217
|
|
|
223
218
|
/**
|
|
@@ -228,31 +223,17 @@ export const RELAY_ADMISSION_CLOSE_TIMEOUT_MS = 2_000;
|
|
|
228
223
|
* `network.unauthorizedRelayReservationCap` (0 refuses every unauthorized
|
|
229
224
|
* reservation — the strict pre-seam posture).
|
|
230
225
|
*
|
|
231
|
-
* NOTE: this cap shares the relay server's own reservation store, whose
|
|
232
|
-
* size is
|
|
233
|
-
* unplaced peers may therefore occupy up to this many of
|
|
234
|
-
* rest are what members and delegates compete for.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
226
|
+
* NOTE: this cap shares the relay server's own reservation store, whose
|
|
227
|
+
* party-run default size is `PARTY_RELAY_MAX_RESERVATIONS` (128,
|
|
228
|
+
* `relay-server.ts`); unplaced peers may therefore occupy up to this many of
|
|
229
|
+
* those slots, and the rest are what members and delegates compete for. The two
|
|
230
|
+
* are kept apart by hand — if this is raised, or an embedder shrinks the store
|
|
231
|
+
* through `network.relayServerInit.reservations.maxReservations`, keep this one
|
|
232
|
+
* well under the store's, or a fleet of unplaceable peers can crowd genuine
|
|
233
|
+
* members out of the server's own store (which the gate cannot override).
|
|
238
234
|
*/
|
|
239
235
|
export const MAX_UNAUTHORIZED_RELAY_RESERVATIONS = 8;
|
|
240
236
|
|
|
241
|
-
/**
|
|
242
|
-
* How long one {@link UnauthorizedReservationBudget} entry occupies a slot
|
|
243
|
-
* without a refresh. Mirrors the relay server's own default reservation TTL
|
|
244
|
-
* (`@libp2p/circuit-relay-v2`'s `DEFAULT_MAX_RESERVATION_TTL`, 2 h): the server
|
|
245
|
-
* holds an unrefreshed reservation exactly that long, and a live reserver
|
|
246
|
-
* re-requests (re-hitting the admission hook, refreshing its entry) well before
|
|
247
|
-
* expiry — so the live entry count tracks the server's own occupancy without
|
|
248
|
-
* this module reaching into the server's reservation store.
|
|
249
|
-
*
|
|
250
|
-
* NOTE: a mirror, not a live coupling — if the relay server is ever configured
|
|
251
|
-
* with a non-default `reservationTtl` (db-p2p's `relayServerInit`), update this
|
|
252
|
-
* constant alongside it or the budget's occupancy drifts from the server's.
|
|
253
|
-
*/
|
|
254
|
-
export const UNAUTHORIZED_RESERVATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
255
|
-
|
|
256
237
|
/**
|
|
257
238
|
* The connection-level outcome of the admission policy:
|
|
258
239
|
* - `'admit'` — a peer with a legitimate claim on the connection (member,
|
|
@@ -295,9 +276,13 @@ export interface InboundAdmissionPolicy {
|
|
|
295
276
|
* Bounded budget of concurrent relay reservations for peers the membership
|
|
296
277
|
* check could not place. `tryAdmit` is the whole surface: an already-admitted
|
|
297
278
|
* peer refreshes its entry (a reservation refresh never double-counts), a new
|
|
298
|
-
* peer takes a free slot or is refused. Entries expire after `ttlMs
|
|
299
|
-
*
|
|
300
|
-
* server
|
|
279
|
+
* peer takes a free slot or is refused. Entries expire after `ttlMs`, which
|
|
280
|
+
* `CadreNode` passes as the relay server's own resolved `reservationTtl`
|
|
281
|
+
* (`relay-server.ts`): the server holds an unrefreshed reservation exactly that
|
|
282
|
+
* long, and a live reserver re-requests (re-hitting the admission hook,
|
|
283
|
+
* refreshing its entry) well before expiry — so the live entry count tracks the
|
|
284
|
+
* server's own occupancy without this module reaching into the server's
|
|
285
|
+
* reservation store. Injectable `now` keeps expiry testable without fake
|
|
301
286
|
* timers. Authorized members and delegates are never run through this — the
|
|
302
287
|
* policy admits them before consulting the budget, and `release`s the slot such
|
|
303
288
|
* a peer took while it was still unplaceable, so the boot-ordering window a
|
|
@@ -307,8 +292,9 @@ export class UnauthorizedReservationBudget {
|
|
|
307
292
|
private readonly admitted = new Map<string, number>();
|
|
308
293
|
|
|
309
294
|
constructor(
|
|
310
|
-
|
|
311
|
-
|
|
295
|
+
/** Most peers admitted at once — `network.unauthorizedRelayReservationCap`, resolved. */
|
|
296
|
+
readonly cap: number = MAX_UNAUTHORIZED_RELAY_RESERVATIONS,
|
|
297
|
+
private readonly ttlMs: number = PARTY_RELAY_RESERVATION_TTL_MS
|
|
312
298
|
) {}
|
|
313
299
|
|
|
314
300
|
/** Number of live (unexpired at last prune) entries — test/diagnostic surface. */
|
package/src/peer-dial.ts
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import debug from 'debug';
|
|
27
|
-
import type { Connection } from '@libp2p/interface';
|
|
28
|
-
import
|
|
27
|
+
import type { Connection, PeerId } from '@libp2p/interface';
|
|
28
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
29
|
+
import type { Component, Multiaddr } from '@multiformats/multiaddr';
|
|
29
30
|
import { withDeadline } from './control-stream.js';
|
|
30
31
|
import { relayedDialBudgetMs } from './link-budget.js';
|
|
31
32
|
|
|
@@ -33,8 +34,9 @@ const log = debug('sereus:cadre:peer-dial');
|
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Default limit on ONE address's dial attempt, in ms, at the DEFAULT declared link round trip:
|
|
36
|
-
* `RELAYED_DIAL_ROUND_TRIPS` (4) x `DECLARED_LINK_ROUND_TRIP_MS` (3500 ms)
|
|
37
|
-
*
|
|
37
|
+
* `RELAYED_DIAL_ROUND_TRIPS` (4) x `DECLARED_LINK_ROUND_TRIP_MS` (3500 ms) plus the called
|
|
38
|
+
* machine's `ADMISSION_DECISION_TIMEOUT_MS` (2000 ms) = 16 000 ms (`relayedDialBudgetMs`). It
|
|
39
|
+
* was a fixed 8000 ms before it was derived.
|
|
38
40
|
*
|
|
39
41
|
* It has to cover the slowest address that should succeed, because an attempt
|
|
40
42
|
* that needs longer fails the same way on every retry. That is a relayed dial on
|
|
@@ -117,9 +119,28 @@ export const DEFAULT_PEER_DIAL_BUDGET: Readonly<PeerDialBudget> = {
|
|
|
117
119
|
|
|
118
120
|
/** The slice of a libp2p node {@link dialPeerAddrs} uses. */
|
|
119
121
|
export interface AddrDialer {
|
|
122
|
+
/** This node's id, to recognise an address that relays through this node ({@link relaysThrough}). */
|
|
123
|
+
readonly peerId: PeerId;
|
|
120
124
|
dial(addr: Multiaddr, options: { signal: AbortSignal }): Promise<Connection>;
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Thrown by {@link dialPeerAddrs} when it was given addresses but every one reaches the peer by
|
|
129
|
+
* relaying through this node, so none was dialed.
|
|
130
|
+
*
|
|
131
|
+
* This is where a relay lands for a peer that holds a reservation on it (a phone, or any node that
|
|
132
|
+
* cannot listen): the address that peer publishes is `/<relay transport>/p2p/<relay>/p2p-circuit/p2p/<peer>`,
|
|
133
|
+
* and dialing it from the relay itself fails with libp2p's `Can not dial self`. Nothing this node
|
|
134
|
+
* does can repair it; once the peer's connection drops, only the peer can reconnect. Callers log it
|
|
135
|
+
* as that rather than as a dial failure.
|
|
136
|
+
*/
|
|
137
|
+
export class SelfRelayOnlyError extends Error {
|
|
138
|
+
constructor(label: string) {
|
|
139
|
+
super(`${label}: every candidate address relays through this node; only the peer can reconnect`);
|
|
140
|
+
this.name = 'SelfRelayOnlyError';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
/** One candidate's outcome, kept so the thrown error can name every attempt. */
|
|
124
145
|
interface AddrAttemptFailure {
|
|
125
146
|
addr: Multiaddr;
|
|
@@ -131,23 +152,31 @@ interface AddrAttemptFailure {
|
|
|
131
152
|
* first connection that forms. Direct addresses are tried before relayed ones
|
|
132
153
|
* ({@link directBeforeRelayed}); otherwise the given order is kept.
|
|
133
154
|
*
|
|
155
|
+
* Addresses that relay through this node are dropped first ({@link relaysThrough}):
|
|
156
|
+
* dialing one could only fail with `Can not dial self`. When that leaves nothing
|
|
157
|
+
* of a non-empty list, this throws {@link SelfRelayOnlyError} without dialing.
|
|
158
|
+
*
|
|
134
159
|
* Addresses naming a transport this node lacks are not filtered out first: a
|
|
135
160
|
* one-address `dial()` rejects those at once (`NoValidAddressesError`), so they
|
|
136
161
|
* cost a log line, not time.
|
|
137
162
|
*
|
|
138
|
-
*
|
|
163
|
+
* Otherwise throws when no address connects — see {@link tryAddrsInTurn} for the error.
|
|
139
164
|
*
|
|
140
165
|
* @param label Names the dial in timeout messages and logs, e.g.
|
|
141
166
|
* `reconcileControlCohort dial of sibling <peerId>`; each attempt reads
|
|
142
167
|
* `<label> via <addr>`.
|
|
143
168
|
*/
|
|
144
|
-
export function dialPeerAddrs(
|
|
169
|
+
export async function dialPeerAddrs(
|
|
145
170
|
dialer: AddrDialer,
|
|
146
171
|
addrs: readonly Multiaddr[],
|
|
147
172
|
budget: PeerDialBudget,
|
|
148
173
|
label: string,
|
|
149
174
|
): Promise<Connection> {
|
|
150
|
-
|
|
175
|
+
const dialable = withoutSelfRelayed(dialer, addrs, label);
|
|
176
|
+
if (addrs.length > 0 && dialable.length === 0) {
|
|
177
|
+
throw new SelfRelayOnlyError(label);
|
|
178
|
+
}
|
|
179
|
+
return tryAddrsInTurn(directBeforeRelayed(dialable), budget, label, (addr, signal) => dialer.dial(addr, { signal }));
|
|
151
180
|
}
|
|
152
181
|
|
|
153
182
|
/**
|
|
@@ -221,6 +250,51 @@ export function directBeforeRelayed(addrs: readonly Multiaddr[]): Multiaddr[] {
|
|
|
221
250
|
return [...addrs.filter((addr) => !relayed(addr)), ...addrs.filter(relayed)];
|
|
222
251
|
}
|
|
223
252
|
|
|
253
|
+
/** `addrs` without those that relay through `dialer` itself, each dropped one logged. */
|
|
254
|
+
function withoutSelfRelayed(dialer: AddrDialer, addrs: readonly Multiaddr[], label: string): Multiaddr[] {
|
|
255
|
+
return addrs.filter((addr) => {
|
|
256
|
+
if (!relaysThrough(addr, dialer.peerId)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
log('%s: skipping %s — it relays through this node', label, addr.toString());
|
|
260
|
+
return false;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Whether `addr` reaches its target by relaying through `relay` at any hop.
|
|
266
|
+
*
|
|
267
|
+
* A circuit address names its relay in the `p2p` component immediately before
|
|
268
|
+
* the `p2p-circuit` marker (`/<transport>/p2p/<relay>/p2p-circuit/p2p/<target>`),
|
|
269
|
+
* so the components are walked rather than the text searched: the target's id
|
|
270
|
+
* after the marker, a bare `/p2p-circuit` naming no relay, and a multi-hop chain
|
|
271
|
+
* each read correctly that way. Same rule as db-p2p's `routesThroughRelay`, which
|
|
272
|
+
* its package does not export.
|
|
273
|
+
*/
|
|
274
|
+
function relaysThrough(addr: Multiaddr, relay: PeerId): boolean {
|
|
275
|
+
const components = addr.getComponents();
|
|
276
|
+
return components.some((component, i) => component.name === 'p2p-circuit' && namesPeer(components[i - 1], relay));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Whether `component` is a `p2p` component naming `peer`. Compared as canonical
|
|
281
|
+
* strings because a multiaddr keeps a peer id in whichever form it was written
|
|
282
|
+
* (base58 or CIDv1). `PeerId.equals` is avoided because test doubles supply the
|
|
283
|
+
* dialer's `peerId` with `toString` only.
|
|
284
|
+
*/
|
|
285
|
+
function namesPeer(component: Component | undefined, peer: PeerId): boolean {
|
|
286
|
+
if (component?.name !== 'p2p' || component.value === undefined) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const expected = peer.toString();
|
|
290
|
+
try {
|
|
291
|
+
return peerIdFromString(component.value).toString() === expected;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
log('cannot read %s as a peer id; treating it as not this node: %o', component.value, err);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
224
298
|
function allAttemptsFailed(label: string, failures: AddrAttemptFailure[]): Error {
|
|
225
299
|
if (failures.length === 0) {
|
|
226
300
|
return new Error(`${label}: no candidate addresses`);
|
|
@@ -63,11 +63,12 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
63
63
|
// `packages/integration-tests/src/scenarios/relayed-dial-cost-by-latency.integration.ts`.
|
|
64
64
|
//
|
|
65
65
|
// It does NOT work at every speed. Above the declared link, libp2p's own dial and
|
|
66
|
-
// inbound-upgrade limits —
|
|
67
|
-
// any deadline here is consulted, and the listener's one makes that failure
|
|
68
|
-
// peer rather than a timeout. That is also what happens when the machine
|
|
69
|
-
// declared a FASTER link than this one, since its limit is the listener's. See
|
|
70
|
-
// `link-budget.ts` ("
|
|
66
|
+
// inbound-upgrade limits — which Optimystic derives from the same declaration — abandon the
|
|
67
|
+
// connection before any deadline here is consulted, and the listener's one makes that failure
|
|
68
|
+
// look like an absent peer rather than a timeout. That is also what happens when the machine
|
|
69
|
+
// being caught up declared a FASTER link than this one, since its limit is the listener's. See
|
|
70
|
+
// `link-budget.ts` ("Optimystic's deadlines, from the same declaration", "What still fails at
|
|
71
|
+
// the supported link").
|
|
71
72
|
//
|
|
72
73
|
// RETRY, and why it backs off. A run whose PUSH FAILED — the transport threw, which is what
|
|
73
74
|
// a dial or response deadline expiring looks like here — re-arms on a doubling backoff
|
|
@@ -176,7 +177,8 @@ export interface PeerJoinBackfillConfig {
|
|
|
176
177
|
maxChunkBlocks?: number;
|
|
177
178
|
/**
|
|
178
179
|
* Per-push dial deadline, ms. Default {@link peerJoinPushBudget}'s `dialTimeoutMs` — four link
|
|
179
|
-
* round trips at the declared link
|
|
180
|
+
* round trips at the declared link plus the called machine's admission decision, 16 000 ms as
|
|
181
|
+
* shipped. NOT a fixed number: a relayed dial
|
|
180
182
|
* costs a fixed number of exchanges, so a host on a slower link moves this (and every other
|
|
181
183
|
* cadre dial budget) by declaring `NetworkConfig.linkRoundTripMs`. Naming it here still wins
|
|
182
184
|
* over the derived value — `link-budget.ts` has the counts and the measurement.
|
package/src/push-fanout.ts
CHANGED
|
@@ -198,6 +198,12 @@ export class PushFanoutService {
|
|
|
198
198
|
* double-wake). Only a dial/transport REJECTION means the phone is suspended
|
|
199
199
|
* and unreachable over the control network, which is the one case that falls
|
|
200
200
|
* through to FCM/APNs.
|
|
201
|
+
*
|
|
202
|
+
* NOTE: the platform push waits for the whole direct dial to fail, up to
|
|
203
|
+
* `DEFAULT_WAKE_DIAL_BUDGET_MS` (46 s at the default declared link). A suspended
|
|
204
|
+
* phone typically fails its relay address fast and spends the rest on its direct
|
|
205
|
+
* one. If that delay shows up, start the platform push in parallel with the dial,
|
|
206
|
+
* or after its first attempt.
|
|
201
207
|
*/
|
|
202
208
|
private async wakePeer(peerId: string, strandId: string, reason: string): Promise<void> {
|
|
203
209
|
// A wake of either kind is now being attempted → arm the cooldown up front.
|
package/src/relay-reservation.ts
CHANGED
|
@@ -143,7 +143,9 @@ export interface RelayReservationState {
|
|
|
143
143
|
/**
|
|
144
144
|
* How long {@link driveRelayReservation} waits for a reservation to appear, at the DEFAULT
|
|
145
145
|
* declared link round trip: `RELAY_RESERVATION_ROUND_TRIPS` (4) x `DECLARED_LINK_ROUND_TRIP_MS`
|
|
146
|
-
* (3500 ms)
|
|
146
|
+
* (3500 ms), plus 2 x `ADMISSION_DECISION_TIMEOUT_MS` (2000 ms) for a party-run relay's
|
|
147
|
+
* connection and reservation decisions, = 18 000 ms (`relayReservationBudgetMs`), where it was a
|
|
148
|
+
* fixed 10_000 before it was derived.
|
|
147
149
|
*
|
|
148
150
|
* Counted rather than chosen because this one deadline bounds the whole drive — dial the relay,
|
|
149
151
|
* request the reservation, wait — and each of those costs a fixed number of exchanges, not a
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The circuit-relay SERVER a party-run node runs — whether it runs one, and with which
|
|
3
|
+
* `@libp2p/circuit-relay-v2` init — resolved once from `NetworkConfig` for both node
|
|
4
|
+
* kinds: the control node (`cadre-node.ts` → `buildControlNodeOptions`) and every strand
|
|
5
|
+
* node (`strand-instance-manager.ts` → `buildStrandRuntime`). One function decides both
|
|
6
|
+
* answers so the two build sites cannot drift apart.
|
|
7
|
+
*
|
|
8
|
+
* WHY cadre sets an init at all. libp2p's server defaults `applyDefaultLimit` to true,
|
|
9
|
+
* which stamps every reservation with `Limit { data: 128 KiB, duration: 2 min }`; libp2p
|
|
10
|
+
* marks each connection relayed under it "limited" and resets it once either cap is
|
|
11
|
+
* reached, and db-p2p's database protocols refuse a limited connection outright (they do
|
|
12
|
+
* not set `runOnLimitedConnection`). A party-run relay exists to carry that party's own
|
|
13
|
+
* database traffic, so the cap cut off every sync and chat history forwarded through it
|
|
14
|
+
* (gotchoices/sereus#19). The dedicated relays (`ops/docker/libp2p-infra`, and the
|
|
15
|
+
* `startDedicatedRelay` test fixture that stands in for it) already turn the limit off;
|
|
16
|
+
* this is the same policy for the party-run kind.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { CircuitRelayServerInit, ServerReservationStoreInit } from '@libp2p/circuit-relay-v2';
|
|
20
|
+
import type { NetworkConfig, NodeProfile } from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default size of a party-run relay's reservation store, raised from libp2p's 15.
|
|
24
|
+
*
|
|
25
|
+
* A NAT'd machine reserves one slot for its control node and one for each strand it
|
|
26
|
+
* serves, all on the same relay (`strand-network-config.ts` hands strand nodes the
|
|
27
|
+
* control node's `network.relayAddrs`), so the store needs
|
|
28
|
+
* `NAT'd machines × (1 + strands each)` slots. At libp2p's 15, three phones serving four
|
|
29
|
+
* strands each already fill it. 128 covers, for example, ten NAT'd machines serving eleven
|
|
30
|
+
* strands each with the unauthorized-reservation budget's 8 slots
|
|
31
|
+
* (`MAX_UNAUTHORIZED_RELAY_RESERVATIONS`) still free on top — one party's worth, where the
|
|
32
|
+
* dedicated relay's 500 is sized for many parties. A reservation costs the server a map
|
|
33
|
+
* entry and a timer, not a connection's worth of state, so the headroom is cheap.
|
|
34
|
+
*/
|
|
35
|
+
export const PARTY_RELAY_MAX_RESERVATIONS = 128;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How long a party-run relay holds a reservation that is not refreshed: libp2p's own
|
|
39
|
+
* default (2 h), set EXPLICITLY so that cadre, not a libp2p constant it cannot import,
|
|
40
|
+
* owns the value the unauthorized-reservation budget has to agree with (see
|
|
41
|
+
* {@link ResolvedRelayServer.init}).
|
|
42
|
+
*/
|
|
43
|
+
export const PARTY_RELAY_RESERVATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The reservation-store settings every party-run relay resolves unless the caller
|
|
47
|
+
* overrides them one key at a time through `network.relayServerInit.reservations`.
|
|
48
|
+
* `applyDefaultLimit: false` is the point of this module — see the module doc.
|
|
49
|
+
*/
|
|
50
|
+
const PARTY_RELAY_RESERVATION_DEFAULTS = {
|
|
51
|
+
applyDefaultLimit: false,
|
|
52
|
+
maxReservations: PARTY_RELAY_MAX_RESERVATIONS,
|
|
53
|
+
reservationTtl: PARTY_RELAY_RESERVATION_TTL_MS
|
|
54
|
+
} as const satisfies ServerReservationStoreInit;
|
|
55
|
+
|
|
56
|
+
/** Reservation settings with the three keys cadre always resolves made definite. */
|
|
57
|
+
export type ResolvedRelayReservations = ServerReservationStoreInit
|
|
58
|
+
& Required<Pick<ServerReservationStoreInit, keyof typeof PARTY_RELAY_RESERVATION_DEFAULTS>>;
|
|
59
|
+
|
|
60
|
+
/** What {@link resolveRelayServer} decides for one node. */
|
|
61
|
+
export interface ResolvedRelayServer {
|
|
62
|
+
/** Does this node run the relay server? Passed as db-p2p's `relay`. */
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The server's init, passed as db-p2p's `relayServerInit` when {@link enabled}: the
|
|
66
|
+
* party-run defaults with the caller's `network.relayServerInit` merged over them.
|
|
67
|
+
* `reservations.reservationTtl` is always set, which is what lets the control node's
|
|
68
|
+
* unauthorized-reservation budget expire its entries on the server's own clock.
|
|
69
|
+
*/
|
|
70
|
+
init: CircuitRelayServerInit & { reservations: ResolvedRelayReservations };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the relay server for a node built from `network` with `profile`.
|
|
75
|
+
*
|
|
76
|
+
* `enabled`: an explicit `network.enableRelay` wins; otherwise storage-profile nodes run
|
|
77
|
+
* the server (they are the machines with the connectivity and uptime to be a relay).
|
|
78
|
+
*
|
|
79
|
+
* `init`: MERGED, not replaced. `reservations` is merged key by key over the party-run
|
|
80
|
+
* defaults, so a caller who sets only `maxReservations` does not silently get the data
|
|
81
|
+
* limit back; every other top-level key is taken from the caller as given. A key set to
|
|
82
|
+
* `undefined` counts as unset, so the default beneath it holds.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveRelayServer(network: NetworkConfig | undefined, profile: NodeProfile): ResolvedRelayServer {
|
|
85
|
+
const override = network?.relayServerInit;
|
|
86
|
+
return {
|
|
87
|
+
enabled: network?.enableRelay ?? (profile === 'storage'),
|
|
88
|
+
init: {
|
|
89
|
+
...override,
|
|
90
|
+
reservations: { ...PARTY_RELAY_RESERVATION_DEFAULTS, ...definedEntries(override?.reservations) }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `obj` without its `undefined`-valued keys, so spreading it never erases a default. */
|
|
96
|
+
function definedEntries<T extends object>(obj: T | undefined): Partial<T> {
|
|
97
|
+
return Object.fromEntries(Object.entries(obj ?? {}).filter(([, value]) => value !== undefined)) as Partial<T>;
|
|
98
|
+
}
|