@serfab/cadre-core 1.8.0 → 1.10.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 (126) hide show
  1. package/dist/cadre-node.d.ts +301 -121
  2. package/dist/cadre-node.js +588 -244
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/control-authorization.d.ts +2 -2
  5. package/dist/control-authorization.js +1 -0
  6. package/dist/control-authorization.js.map +1 -1
  7. package/dist/control-cohort.d.ts +1 -1
  8. package/dist/control-cohort.js +1 -1
  9. package/dist/control-database.d.ts +105 -14
  10. package/dist/control-database.js +242 -73
  11. package/dist/control-database.js.map +1 -1
  12. package/dist/control-formation-recorder.d.ts +6 -6
  13. package/dist/control-formation-recorder.js +7 -8
  14. package/dist/control-formation-recorder.js.map +1 -1
  15. package/dist/control-schema.d.ts +1 -1
  16. package/dist/control-schema.js +90 -3
  17. package/dist/control-schema.js.map +1 -1
  18. package/dist/control-write-retry.d.ts +21 -15
  19. package/dist/control-write-retry.js +21 -15
  20. package/dist/control-write-retry.js.map +1 -1
  21. package/dist/index.d.ts +7 -7
  22. package/dist/index.js +16 -13
  23. package/dist/index.js.map +1 -1
  24. package/dist/invitation-bootstrap.d.ts +32 -0
  25. package/dist/invitation-bootstrap.js +30 -0
  26. package/dist/invitation-bootstrap.js.map +1 -0
  27. package/dist/link-budget.d.ts +204 -119
  28. package/dist/link-budget.js +208 -120
  29. package/dist/link-budget.js.map +1 -1
  30. package/dist/membership-connection-gater.d.ts +3 -3
  31. package/dist/membership-connection-gater.js +3 -3
  32. package/dist/node-local-snapshot.d.ts +6 -4
  33. package/dist/node-local-snapshot.js +7 -3
  34. package/dist/node-local-snapshot.js.map +1 -1
  35. package/dist/peer-dial.d.ts +18 -14
  36. package/dist/peer-dial.js +18 -14
  37. package/dist/peer-dial.js.map +1 -1
  38. package/dist/peer-join-backfill.d.ts +3 -12
  39. package/dist/peer-join-backfill.js +2 -3
  40. package/dist/peer-join-backfill.js.map +1 -1
  41. package/dist/pending-join-runner.d.ts +186 -0
  42. package/dist/pending-join-runner.js +649 -0
  43. package/dist/pending-join-runner.js.map +1 -0
  44. package/dist/push-fanout.d.ts +1 -1
  45. package/dist/push-fanout.js +1 -1
  46. package/dist/seed-bootstrap.d.ts +2 -2
  47. package/dist/seed-bootstrap.js +1 -1
  48. package/dist/strand-addr-protocol.js +2 -2
  49. package/dist/strand-formation-deadlines.d.ts +5 -5
  50. package/dist/strand-formation-deadlines.js +5 -5
  51. package/dist/strand-formation-manager.d.ts +18 -4
  52. package/dist/strand-formation-manager.js +48 -33
  53. package/dist/strand-formation-manager.js.map +1 -1
  54. package/dist/strand-formation-protocol.d.ts +71 -25
  55. package/dist/strand-formation-protocol.js +225 -51
  56. package/dist/strand-formation-protocol.js.map +1 -1
  57. package/dist/strand-formation-rejection.d.ts +94 -0
  58. package/dist/strand-formation-rejection.js +77 -0
  59. package/dist/strand-formation-rejection.js.map +1 -0
  60. package/dist/strand-fret-addrs.d.ts +54 -0
  61. package/dist/strand-fret-addrs.js +108 -0
  62. package/dist/strand-fret-addrs.js.map +1 -0
  63. package/dist/strand-instance-manager.d.ts +9 -38
  64. package/dist/strand-instance-manager.js +17 -74
  65. package/dist/strand-instance-manager.js.map +1 -1
  66. package/dist/strand-network-state-file.d.ts +12 -0
  67. package/dist/strand-network-state-file.js +38 -0
  68. package/dist/strand-network-state-file.js.map +1 -0
  69. package/dist/strand-network-state.d.ts +111 -0
  70. package/dist/strand-network-state.js +191 -0
  71. package/dist/strand-network-state.js.map +1 -0
  72. package/dist/strand-solicitation.d.ts +11 -3
  73. package/dist/strand-solicitation.js +9 -0
  74. package/dist/strand-solicitation.js.map +1 -1
  75. package/dist/types.d.ts +123 -40
  76. package/dist/types.js +5 -9
  77. package/dist/types.js.map +1 -1
  78. package/package.json +25 -24
  79. package/src/cadre-node.ts +637 -251
  80. package/src/control-authorization.ts +117 -116
  81. package/src/control-cohort.ts +1 -1
  82. package/src/control-database.ts +303 -82
  83. package/src/control-formation-recorder.ts +8 -9
  84. package/src/control-schema.ts +90 -3
  85. package/src/control-write-retry.ts +21 -15
  86. package/src/index.ts +36 -56
  87. package/src/invitation-bootstrap.ts +40 -0
  88. package/src/link-budget.ts +221 -120
  89. package/src/membership-connection-gater.ts +3 -3
  90. package/src/node-local-snapshot.ts +12 -7
  91. package/src/peer-dial.ts +18 -14
  92. package/src/peer-join-backfill.ts +5 -6
  93. package/src/pending-join-runner.ts +735 -0
  94. package/src/push-fanout.ts +1 -1
  95. package/src/seed-bootstrap.ts +2 -2
  96. package/src/strand-addr-protocol.ts +2 -2
  97. package/src/strand-formation-deadlines.ts +5 -5
  98. package/src/strand-formation-manager.ts +62 -37
  99. package/src/strand-formation-protocol.ts +1036 -803
  100. package/src/strand-formation-rejection.ts +129 -0
  101. package/src/strand-fret-addrs.ts +145 -0
  102. package/src/strand-instance-manager.ts +24 -97
  103. package/src/strand-network-state-file.ts +40 -0
  104. package/src/strand-network-state.ts +243 -0
  105. package/src/strand-solicitation.ts +17 -4
  106. package/src/types.ts +119 -40
  107. package/dist/strand-peer-book-file.d.ts +0 -12
  108. package/dist/strand-peer-book-file.js +0 -37
  109. package/dist/strand-peer-book-file.js.map +0 -1
  110. package/dist/strand-peer-book-protocol.d.ts +0 -168
  111. package/dist/strand-peer-book-protocol.js +0 -356
  112. package/dist/strand-peer-book-protocol.js.map +0 -1
  113. package/dist/strand-peer-book-swap.d.ts +0 -104
  114. package/dist/strand-peer-book-swap.js +0 -284
  115. package/dist/strand-peer-book-swap.js.map +0 -1
  116. package/dist/strand-peer-book.d.ts +0 -159
  117. package/dist/strand-peer-book.js +0 -361
  118. package/dist/strand-peer-book.js.map +0 -1
  119. package/dist/strand-peer-observer.d.ts +0 -88
  120. package/dist/strand-peer-observer.js +0 -180
  121. package/dist/strand-peer-observer.js.map +0 -1
  122. package/src/strand-peer-book-file.ts +0 -39
  123. package/src/strand-peer-book-protocol.ts +0 -466
  124. package/src/strand-peer-book-swap.ts +0 -338
  125. package/src/strand-peer-book.ts +0 -475
  126. package/src/strand-peer-observer.ts +0 -235
@@ -58,66 +58,87 @@
58
58
  *
59
59
  * ── The counts, and what they cost ──
60
60
  *
61
- * | operation | link round trips | at the declared default |
62
- * | ---------------------------------------------------- | ---------------- | ----------------------- |
63
- * | open a relayed connection to another machine | 4 | 14 000 ms |
64
- * | dial the relay itself | 1 | 3 500 ms |
65
- * | request a reservation on an open relay connection | 1 | 3 500 ms |
66
- * | negotiate a protocol over an established circuit | 1 | 3 500 ms |
67
- * | one request and its answer over an open circuit | 2 | 7 000 ms |
68
- * | open a relayed connection, then negotiate one protocol| 5 | 17 500 ms |
69
- * | open a relayed connection, then one request on it | 6 | 21 000 ms |
70
- * | one Optimystic commit over connections already open | 20 | 70 000 ms |
71
- *
72
- * The reservation-request row is {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS}, which also sizes how long
61
+ * | operation | link round trips | at the declared default |
62
+ * | ---------------------------------------------------------------- | ---------------- | ----------------------- |
63
+ * | open a relayed connection over a relay connection already open | 4 | 14 000 ms |
64
+ * | dial the relay itself | 1 | 3 500 ms |
65
+ * | open a relayed connection, opening the relay connection first | 5 | 17 500 ms |
66
+ * | request a reservation on an open relay connection | 1 | 3 500 ms |
67
+ * | negotiate a protocol over an established circuit | 1 | 3 500 ms |
68
+ * | one request and its answer over an open circuit | 2 | 7 000 ms |
69
+ * | open a relayed connection, then negotiate one protocol | 6 | 21 000 ms |
70
+ * | open a relayed connection, then one request on it | 7 | 24 500 ms |
71
+ * | one Optimystic commit over connections already open | 20 | 70 000 ms |
72
+ *
73
+ * Every row that opens a relayed connection counts opening the relay connection too
74
+ * ({@link RELAYED_DIAL_ROUND_TRIPS}), because a dialer cannot know beforehand whether it holds
75
+ * one, and a budget that is too short fails as silently as an absent peer. The
76
+ * reservation-request row is {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS}, which also sizes how long
73
77
  * a party-run relay waits for a stranger's reservation ({@link relayAdmissionReserveDeadlineMs}).
74
78
  * The commit row is a measurement of a whole write, not a count of exchanges; its doc comment
75
79
  * ({@link COMMIT_ROUND_TRIPS}) holds the figures and says when to re-measure it.
76
80
  *
77
- * **Measured** 2026-09-26, one Windows machine, loopback dedicated relay: a relayed dial took
78
- * 20-25 ms at no delay, **7 255-7 279 ms at 900 ms one-way**, and **12 066-12 094 ms at 1 500 ms
79
- * one-way**. Four link round trips of pure delay would be 7 200 and 12 000, so the real cost
80
- * sits about 60-95 ms ABOVE the arithmetic — the handshakes, which no delay figure contains.
81
- * That residue is why {@link DECLARED_LINK_ROUND_TRIP_MS} carries headroom over the link sereus
82
- * supports rather than matching it exactly: a declaration equal to the supported round trip
83
- * would derive a budget marginally BELOW the dial it has to contain.
84
- *
85
- * ── The listener's admission decision ──
81
+ * **Measured** 2026-09-26, one Windows machine, loopback dedicated relay: a relayed dial over a
82
+ * relay connection the dialer already held took 20-25 ms at no delay, **7 255-7 279 ms at 900 ms
83
+ * one-way**, and **12 066-12 094 ms at 1 500 ms one-way**. Four link round trips of pure delay
84
+ * would be 7 200 and 12 000, so the real cost sits about 60-95 ms ABOVE the arithmetic — the
85
+ * handshakes, which no delay figure contains. On 2026-10-01 (libp2p 3.3.11), a dialer holding no
86
+ * relay connection took **15 113 ms at 1 500 ms one-way**: ten one-way delays, five link round
87
+ * trips, 113 ms above the arithmetic. That residue is why {@link DECLARED_LINK_ROUND_TRIP_MS}
88
+ * carries headroom over the link sereus supports rather than matching it exactly: a declaration
89
+ * equal to the supported round trip would derive a budget marginally BELOW the dial it has to
90
+ * contain.
91
+ *
92
+ * NOTE: the instrument splits the delay evenly across both hops (each node's own dialed socket
93
+ * gets the same one-way figure). On a link whose whole round trip sits on one machine's hop to
94
+ * the relay (a phone on a congested mobile link), the exchanges that cross only that hop — the
95
+ * relay dial, the hop-stream negotiation and CONNECT — may cost up to twice their share of the
96
+ * count. Not measured. If relayed dials are seen failing near their budget on such a link,
97
+ * measure that split before raising any count or the declaration.
98
+ *
99
+ * ── Admission decisions ──
86
100
  *
87
101
  * The table is link time only. Every budget that OPENS a connection also adds
88
- * {@link ADMISSION_DECISION_TIMEOUT_MS} (2 000 ms) per admission decision the machine being
89
- * called may make on the way, because that machine's gate runs on the dialer's clock (the
90
- * constant's doc says why). So a relayed dial is budgeted at 14 000 + 2 000 = 16 000 ms at the
91
- * default declaration, a dial plus one request at 23 000 ms, and a reservation drive, which a
92
- * party-run relay decides twice, at 18 000 ms. The allowance is flat: it is local decision
93
- * time, not link time, so it does not scale with the declaration.
94
- *
95
- * The measurement above crossed no gate. The instrument is bare libp2p on both sides, with no
96
- * cadre connection gater, so the 12 094 ms contains no admission decision at all. The
97
- * allowance is therefore on top of the measured dial rather than already inside it.
98
- *
99
- * NOTE: the measured dial also reused a connection to the relay that the dialer already held
100
- * (the instrument dials the relay first). A relayed dial that has to open that connection itself
101
- * spends one more link round trip, plus the relay's own admission decision when the relay is a
102
- * party-run control node, and the count counts neither; at the supported link such a dial fits
103
- * only while the called machine decides in under about 0.9 s.
104
- * `bug-relayed-dial-budget-omits-opening-the-relay-connection`.
102
+ * {@link ADMISSION_DECISION_TIMEOUT_MS} (2 000 ms) once for each gate the open may pass, because
103
+ * each gate runs on the dialer's clock (the constant's doc says why). A relayed dial passes
104
+ * {@link DIAL_ADMISSION_DECISIONS}, two: a party-run relay's, on the dialer's connection to it,
105
+ * then the called machine's. So a relayed dial is budgeted at 17 500 + 2 x 2 000 = 21 500 ms at
106
+ * the default declaration, a dial plus one request at 28 500 ms, and a reservation drive, which
107
+ * a party-run relay decides twice, at 18 000 ms. The allowance is flat: it is local decision
108
+ * time, not link time, so it does not scale with the declaration. One allowance per gate, rather
109
+ * than slack in the count, is what keeps each budget valid at every declaration.
110
+ *
111
+ * The measurement above crossed no gate. The instrument is bare libp2p on every side, with no
112
+ * cadre connection gater, so the 15 113 ms contains no admission decision at all. The
113
+ * allowances are therefore on top of the measured dial rather than already inside it.
105
114
  *
106
115
  * ── Optimystic's deadlines, from the same declaration ──
107
116
  *
108
117
  * cadre states its declared link to Optimystic on the control node and every strand node
109
- * (`@optimystic/db-p2p`'s `NodeOptions.linkRoundTripMs`, from 1.8.0), and Optimystic derives
110
- * its network deadlines from it (`resolveLinkDeadlines` in its `rpc-deadline.ts`), each never
111
- * below the LAN value it replaced. At the default declaration:
112
- *
113
- * | Optimystic deadline | link round trips | at the default | undeclared |
114
- * | ---------------------------------------------------- | ---------------- | -------------- | ---------- |
115
- * | libp2p `dialTimeout` and `inboundUpgradeTimeout` | 5 | 17 500 ms | 10 000 ms |
116
- * | one request's dial, stream negotiation included | 6 | 21 000 ms | 3 000 ms |
117
- * | one request's response | 3 | 10 500 ms | 10 000 ms |
118
- * | a rebalance transfer's dial, and again its reply | 6, floor 30 000 | 30 000 ms | 30 000 ms |
119
- *
120
- * Its churn and under-replication pushes take the request rows' values. The undeclared column
118
+ * (`@optimystic/db-p2p`'s `NodeOptions.linkRoundTripMs`), and Optimystic derives its network
119
+ * deadlines from it (`resolveLinkDeadlines` in its `rpc-deadline.ts`), each never below the LAN
120
+ * value it replaced. Optimystic's derivation is link time only, so cadre states the three limits
121
+ * that cover opening a connection itself ({@link optimysticDialLimits}): Optimystic's value plus
122
+ * {@link DIAL_ADMISSION_DECISIONS} admission decisions. At the default declaration:
123
+ *
124
+ * | deadline | link round trips | at the default | undeclared |
125
+ * | ------------------------------------------------------- | ---------------------- | -------------- | ---------- |
126
+ * | libp2p `addressDialTimeout`, one address of a dial | 10, + 2 decisions | 39 000 ms | 6 000 ms |
127
+ * | libp2p `dialTimeout`, a dial that carries no signal | 10, + 2 decisions | 39 000 ms | 10 000 ms |
128
+ * | libp2p `inboundUpgradeTimeout`, the listener's side | 5 | 17 500 ms | 10 000 ms |
129
+ * | one request's dial, stream negotiation included | 11, + 2 decisions | 42 500 ms | 3 000 ms |
130
+ * | one request's response | 3 | 10 500 ms | 10 000 ms |
131
+ * | a rebalance transfer's dial, and again its reply | request dial, ≥ 30 000 | 42 500 ms | 30 000 ms |
132
+ *
133
+ * Ten round trips is Optimystic's count for opening a relayed connection through a relay the
134
+ * dialer is not yet connected to, which opens that connection first (about 8.6 round trips over
135
+ * WebSocket, 9.6 over `wss`), with a margin. The two decisions are the gates that open runs
136
+ * through on the dialer's clock: a party-run relay's, on the dialer's connection to it, then the
137
+ * called machine's. Each may take {@link ADMISSION_DECISION_TIMEOUT_MS}.
138
+ *
139
+ * Its churn and under-replication pushes take the request rows' values, and the Quereus plugin
140
+ * derives a write's budgets from the request dial: four of them for the transaction (170 000 ms)
141
+ * and one for cancelling it (42 500 ms). The undeclared column
121
142
  * is why cadre always states the value, default included. A fixed 3 000 ms request dial cannot
122
143
  * open a relayed connection above 375 ms one-way, and it also bounded the protocol negotiation
123
144
  * on a connection that was already open, which is one link round trip: at the supported link
@@ -128,29 +149,38 @@
128
149
  * ({@link cohortReadDeadlineMs} says why).
129
150
  *
130
151
  * NOTE: the longer deadlines are paid against a machine that is gone. An Optimystic request
131
- * to a peer that never completes a connection now waits up to the 21 000 ms dial deadline where
132
- * it waited 3 000, and one that connects and never answers waits 10 500 where it waited 10 000.
133
- * Measured 2026-09-30 (`control-write-degraded-cohort-member.integration.ts`): a control write
134
- * against a connected, silent member failed at 42.2 s, against 40.2 s with no link declared.
135
- * The dial case was not measured. cadre's cut-off budgets (`CONTROL_WRITE_RETRY_BUDGET_MS`,
136
- * `CONTROL_READ_RETRY_BUDGET_MS`) are checked between attempts, so one attempt can outlast
137
- * them by that much. If writes on a fast link are seen waiting out the dial deadline against an
138
- * unreachable machine, declare that deployment's real round trip instead of the default.
139
- *
140
- * libp2p's two limits bound the same relayed dial as cadre's budgets do: the DIALER's
141
- * `dialTimeout`, which bounds every dial that carries no abort signal of its own, and the
142
- * LISTENER's `inboundUpgradeTimeout`, which is how long the machine being called lets a
143
- * half-built connection finish its handshakes. At libp2p's 10 000 ms, no relayed connection
144
- * could be opened above about 1 250 ms one-way (a 2.5-second link round trip) whatever cadre
145
- * declared, and the listener's limit is the one that makes that failure silent: at 1 500 ms
146
- * one-way the dialer's own 12 094 ms dial RESOLVED while the listener had already thrown the
147
- * connection away at 10 s, so the first stream over it died with `Unexpected EOF - stream
148
- * closed while reading 0/1 bytes` and the listener reported no peer at all. Optimystic's
149
- * derivation contains {@link relayedDialBudgetMs} at every declaration: five round trips are at
150
- * least four plus the {@link ADMISSION_DECISION_TIMEOUT_MS} allowance once the round trip
151
- * reaches 2 000 ms, and the 10 000 ms floor covers every shorter one. `link-budget.spec.ts` pins
152
- * that against Optimystic's own derivation, so a change on either side that breaks it fails
153
- * there rather than silently on a slow link.
152
+ * to a peer that never completes a connection waits up to the 42 500 ms dial deadline, each of
153
+ * that peer's addresses up to 39 000 ms inside it, and one that connects and never answers
154
+ * waits 10 500. A control write against a connected, silent member fails after about 84 s
155
+ * (one pend round and three cancel rounds; `docs/cadre-consistency.md` → "Deadlines Over
156
+ * Optimystic's Reads and Commits"). The dial case was not measured. cadre's cut-off budgets
157
+ * (`CONTROL_WRITE_RETRY_BUDGET_MS`, `CONTROL_READ_RETRY_BUDGET_MS`) are checked between
158
+ * attempts, so one attempt can outlast them by that much. If writes on a fast link are seen
159
+ * waiting out the dial deadline against an unreachable machine, declare that deployment's real
160
+ * round trip instead of the default.
161
+ *
162
+ * The LISTENER's `inboundUpgradeTimeout` is how long the machine being called lets a half-built
163
+ * connection finish its handshakes, and it is the limit that makes a dial outlasting it silent:
164
+ * at libp2p's 10 000 ms and 1 500 ms one-way, the dialer's own 12 094 ms dial RESOLVED while
165
+ * the listener had already thrown the connection away at 10 s, so the first stream over it died
166
+ * with `Unexpected EOF - stream closed while reading 0/1 bytes` and the listener reported no
167
+ * peer at all. cadre leaves it to Optimystic, with no decisions added: its timer starts when the
168
+ * relay hands the listener the inbound circuit, so it runs over the listener's own upgrade and
169
+ * the listener's own decision, never over the dialer's connection to the relay or the relay's
170
+ * decision. So it has to contain the part of {@link relayedDialBudgetMs} its clock covers, not
171
+ * the whole budget: at most the circuit leg ({@link CIRCUIT_DIAL_ROUND_TRIPS}) plus one
172
+ * {@link ADMISSION_DECISION_TIMEOUT_MS}. Optimystic's derivation does at every declaration: five
173
+ * round trips are at least four plus the allowance once the round trip reaches 2 000 ms, and the
174
+ * 10 000 ms floor covers every shorter one. `link-budget.spec.ts` pins that against Optimystic's
175
+ * own derivation, so a change on either side that breaks it fails there rather than silently on
176
+ * a slow link.
177
+ *
178
+ * NOTE: the whole {@link relayedDialBudgetMs} is longer than the listener's limit (21 500 ms
179
+ * against 17 500 ms at the default), so on a link SLOWER than the one declared, a dial over a
180
+ * relay connection the dialer already holds can outlast the listener while its own budget still
181
+ * runs (at the default, between about a 4.4 s and a 5.4 s round trip), and fails silently
182
+ * instead of with a timeout. At or below the declared link it cannot. If streams dying with
183
+ * `Unexpected EOF` are seen on such a link, declare that link's real round trip.
154
184
  *
155
185
  * NOTE: derived from the declaration alone, so a per-field override that raises a cadre dial
156
186
  * budget above it (`controlCohort.perAddressDialTimeoutMs`, `strandBackfill.dialTimeoutMs`)
@@ -174,20 +204,21 @@
174
204
  * arrives.
175
205
  */
176
206
 
207
+ import { resolveLinkDeadlines, type Libp2pConnectionTimeouts, type RpcDeadlineDefaults } from '@optimystic/db-p2p';
208
+
177
209
  /**
178
210
  * The link round trip cadre assumes when a host declares none, in milliseconds. A deployment
179
211
  * that knows its own link states it with `NetworkConfig.linkRoundTripMs` instead.
180
212
  *
181
213
  * **Why 3 500.** Sereus supports two machines that reach each other only through a relay up to
182
214
  * a **3-second link round trip** (1 500 ms each way — a congested mobile or satellite link;
183
- * `docs/architecture.md` → "Relay Integration"). The worst relayed dial measured at that link
184
- * was 12 094 ms, above the 12 000 ms a declaration of exactly 3 000 would derive, so the
185
- * declaration rounds the supported link up to the next half second. 3 500 derives 14 000 ms
186
- * of link time and leaves about 1.9 s over that dial: room for a phone's Noise handshake,
187
- * which the loopback measurement on a desktop does not contain. The listener's admission
188
- * decision is not in that 1.9 s; it has its own allowance on top (the module doc's "The
189
- * listener's admission decision"). (The previous 2 000 followed the same convention for the
190
- * 1.8 s band sereus supported before, with about 720 ms to spare.)
215
+ * `docs/architecture.md` → "Relay Integration"). The worst relayed dial measured at that link,
216
+ * opening the relay connection first, was 15 113 ms, above the 15 000 ms a declaration of
217
+ * exactly 3 000 would derive, so the declaration rounds the supported link up to the next half
218
+ * second. 3 500 derives 17 500 ms of link time and leaves about 2.4 s over that dial: room for a
219
+ * phone's Noise handshakes, which the loopback measurement on a desktop does not contain. The
220
+ * admission decisions are not in that 2.4 s; they have their own allowance on top (the module
221
+ * doc's "Admission decisions").
191
222
  *
192
223
  * One budget outside this module does NOT move with it: `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`
193
224
  * (`strand-first-sync-gate.ts`) bounds a whole first sync, which has no round-trip count, so it
@@ -196,33 +227,38 @@
196
227
  * ({@link cohortReadDeadlineMs}).
197
228
  *
198
229
  * What 3 500 costs, against 2 000: a peer that is genuinely gone holds each operation longer
199
- * before it is abandoned and retried — the link part of a relayed dial or a reservation drive
200
- * 14 s instead of 8 s, and of a control-cohort dial of one peer across all its addresses 56 s
201
- * instead of 32 s. The admission allowance adds to both (see {@link relayedDialBudgetMs}).
230
+ * before it is abandoned and retried — the link part of a relayed dial 17.5 s instead of 10 s,
231
+ * of a reservation drive 14 s instead of 8 s, and of a control-cohort dial of one peer across
232
+ * all its addresses 70 s instead of 40 s. The admission allowances add to each (see
233
+ * {@link relayedDialBudgetMs}).
202
234
  * Raise it further for a link slower still; lower it only on a deployment where EVERY machine
203
235
  * of the party is that close, for the reason the "What still fails" list gives.
204
236
  */
205
237
  export const DECLARED_LINK_ROUND_TRIP_MS = 3500;
206
238
 
207
239
  /**
208
- * Link round trips one relayed connection setup costs: transport, encryption and multiplexer
209
- * handshakes to the relay, the circuit open, then both handshakes again end to end through it.
210
- * Measured at 8 one-way delays, which is 4 link round trips.
240
+ * Link round trips opening a relayed connection costs over a relay connection the dialer
241
+ * ALREADY holds: the circuit open through the relay (hop-stream negotiation and CONNECT, then
242
+ * the relay's stop stream to the called machine), then the encryption and multiplexer
243
+ * handshakes end to end through it. Measured at 8 one-way delays.
211
244
  */
212
- export const RELAYED_DIAL_ROUND_TRIPS = 4;
245
+ export const CIRCUIT_DIAL_ROUND_TRIPS = 4;
213
246
 
214
247
  /**
215
- * Link round trips one whole reservation drive is budgeted for
216
- * (`relay-reservation.ts`'s `driveRelayReservation`, whose ONE deadline covers the dial, the
217
- * reservation request and the wait together).
218
- *
219
- * The protocol work measured is 2 — dial the relay (1) plus request the reservation (1). It is
220
- * budgeted at 4, the relayed-dial count, because the wait afterwards may instead be satisfied
221
- * by libp2p's own relay discovery, which repeats both legs after identify has run; 4 is
222
- * therefore the largest thing this one deadline can be asked to contain, and the poll interval
223
- * (`DEFAULT_RELAY_RESERVE_POLL_MS`) rounds up on top of it.
248
+ * Link round trips the dialer spends opening its own connection to the relay: transport,
249
+ * encryption and multiplexer handshakes on its one hop. Measured at 2 one-way delays. libp2p's
250
+ * circuit transport opens it inside the relayed dial, under the caller's signal, whenever no
251
+ * relay connection is open yet.
252
+ */
253
+ export const RELAY_DIAL_ROUND_TRIPS = 1;
254
+
255
+ /**
256
+ * Link round trips a relayed dial is budgeted for: opening the relay connection
257
+ * ({@link RELAY_DIAL_ROUND_TRIPS}) and then the circuit over it ({@link CIRCUIT_DIAL_ROUND_TRIPS}).
258
+ * Measured at 10 one-way delays from a dialer holding no relay connection. Counted for every
259
+ * relayed dial, because a dialer cannot know beforehand whether that connection is open.
224
260
  */
225
- export const RELAY_RESERVATION_ROUND_TRIPS = 4;
261
+ export const RELAYED_DIAL_ROUND_TRIPS = RELAY_DIAL_ROUND_TRIPS + CIRCUIT_DIAL_ROUND_TRIPS;
226
262
 
227
263
  /**
228
264
  * Link round trips from the moment a party-run relay admits a stranger's connection for relay
@@ -242,6 +278,20 @@ export const RELAY_RESERVATION_ROUND_TRIPS = 4;
242
278
  */
243
279
  export const RELAY_RESERVE_REQUEST_ROUND_TRIPS = 1;
244
280
 
281
+ /**
282
+ * Link round trips one whole reservation drive is budgeted for
283
+ * (`relay-reservation.ts`'s `driveRelayReservation`, whose ONE deadline covers the dial, the
284
+ * reservation request and the wait together).
285
+ *
286
+ * The protocol work measured is 2 — dial the relay ({@link RELAY_DIAL_ROUND_TRIPS}) plus request
287
+ * the reservation ({@link RELAY_RESERVE_REQUEST_ROUND_TRIPS}). It is budgeted at twice that,
288
+ * because the wait afterwards may instead be satisfied by libp2p's own relay discovery, which
289
+ * repeats both legs after identify has run; 4 is therefore the largest thing this one deadline
290
+ * can be asked to contain, and the poll interval (`DEFAULT_RELAY_RESERVE_POLL_MS`) rounds up on
291
+ * top of it.
292
+ */
293
+ export const RELAY_RESERVATION_ROUND_TRIPS = 2 * (RELAY_DIAL_ROUND_TRIPS + RELAY_RESERVE_REQUEST_ROUND_TRIPS);
294
+
245
295
  /**
246
296
  * Link round trips negotiating one protocol over an ALREADY-OPEN connection costs: the
247
297
  * multistream-select exchange that `dialProtocol` / `newStream` runs before the stream is
@@ -290,7 +340,7 @@ export const COMMIT_ROUND_TRIPS = 20;
290
340
  * possibly through a relay: {@link RELAYED_DIAL_ROUND_TRIPS} + {@link CIRCUIT_REQUEST_ROUND_TRIPS}.
291
341
  * The shape of cadre's own one-frame control protocols (strand wake, strand address, seed
292
342
  * delivery), each of which bounds the dial and the exchange with ONE deadline. Link time only:
293
- * {@link relayedRequestBudgetMs} adds the listener's admission allowance on top.
343
+ * {@link relayedRequestBudgetMs} adds the dial's admission allowances on top.
294
344
  */
295
345
  export const RELAYED_REQUEST_ROUND_TRIPS = RELAYED_DIAL_ROUND_TRIPS + CIRCUIT_REQUEST_ROUND_TRIPS;
296
346
 
@@ -350,11 +400,13 @@ export const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
350
400
  * - **The decision is spent on the dialing machine's clock.** libp2p's listener
351
401
  * runs this gate before it answers the multiplexer negotiation the dialer is
352
402
  * waiting on (`libp2p/dist/src/upgrader.js`), and the listener's own
353
- * `inboundUpgradeTimeout` is already running while it does. So
354
- * {@link relayedDialBudgetMs}, and every budget built on it, adds this
355
- * deadline once per decision the called machine may make, and the two libp2p
356
- * limits Optimystic derives from the same declaration contain it (the module
357
- * doc's "Optimystic's deadlines, from the same declaration").
403
+ * `inboundUpgradeTimeout` is already running while it does; a party-run
404
+ * relay runs the same gate on the dialer's connection to it. So
405
+ * {@link relayedDialBudgetMs}, every budget built on it, and the dial limits
406
+ * cadre states to Optimystic ({@link optimysticDialLimits}) add this deadline
407
+ * once for each of the {@link DIAL_ADMISSION_DECISIONS} gates a dial passes,
408
+ * and the listener's limit Optimystic derives contains the called machine's
409
+ * (the module doc's "Optimystic's deadlines, from the same declaration").
358
410
  * Raising it therefore lengthens every dial budget with it: a slow decision
359
411
  * still fits, and a dial to a peer that is gone takes that much longer to
360
412
  * give up.
@@ -393,31 +445,39 @@ export function resolveLinkRoundTripMs(linkRoundTripMs?: number): number {
393
445
  return linkRoundTripMs;
394
446
  }
395
447
 
448
+ /**
449
+ * Admission decisions a relayed dial may wait on, each on the dialer's clock: a party-run relay's,
450
+ * on the dialer's connection to it (`denyInboundEncryptedConnection`), then the called machine's.
451
+ */
452
+ export const DIAL_ADMISSION_DECISIONS = 2;
453
+
396
454
  /**
397
455
  * Deadline for OPENING a connection to another machine that may only be reachable through a
398
- * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip, plus one
399
- * {@link ADMISSION_DECISION_TIMEOUT_MS} for the called machine's inbound gate, which runs on
400
- * this dial's clock. 4 x 3 500 + 2 000 = 16 000 ms at the default declaration.
401
- *
402
- * What the allowance costs: a peer that is truly gone holds each dial 2 s longer before it is
403
- * abandoned — a per-address dial 16 s instead of 14 s, a control-cohort dial of one peer across
404
- * its 4 addresses 64 s instead of 56 s, and a wake call's two attempts 46 s instead of 42 s. A
405
- * machine that runs no gate (the dedicated relay container, an open-strand node) gets the same
406
- * budget; the extra 2 s only lengthens a failure there, never a success.
456
+ * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip, which includes opening
457
+ * the dialer's own connection to the relay, plus one {@link ADMISSION_DECISION_TIMEOUT_MS} for
458
+ * each of the {@link DIAL_ADMISSION_DECISIONS} gates that run on this dial's clock.
459
+ * 5 x 3 500 + 2 x 2 000 = 21 500 ms at the default declaration.
460
+ *
461
+ * What it costs: a peer that is truly gone holds each dial this long before it is abandoned — a
462
+ * per-address dial 21.5 s, a control-cohort dial of one peer across its 4 addresses 86 s, and a
463
+ * wake call's two attempts 57 s — of which the relay leg and the relay's decision are 5.5 s per
464
+ * dial. A dial over a relay connection already open, or to a machine that runs no gate (the
465
+ * dedicated relay container, an open-strand node), gets the same budget; the unused part only
466
+ * lengthens a failure there, never a success.
407
467
  *
408
468
  * A per-field override of a deadline derived from this one
409
469
  * (`network.controlCohort.perAddressDialTimeoutMs`, `strandBackfill.dialTimeoutMs`,
410
- * `DialWakeOptions.timeoutMs`, …) replaces the whole derived value, allowance included.
470
+ * `DialWakeOptions.timeoutMs`, …) replaces the whole derived value, allowances included.
411
471
  */
412
472
  export function relayedDialBudgetMs(linkRoundTripMs?: number): number {
413
- return RELAYED_DIAL_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs) + ADMISSION_DECISION_TIMEOUT_MS;
473
+ return RELAYED_DIAL_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs) + DIAL_ADMISSION_DECISIONS * ADMISSION_DECISION_TIMEOUT_MS;
414
474
  }
415
475
 
416
476
  /**
417
477
  * Deadline for what `dialProtocol` does end to end: OPEN a possibly relayed connection
418
- * ({@link relayedDialBudgetMs}, admission allowance included) and then NEGOTIATE one protocol
419
- * on it ({@link PROTOCOL_NEGOTIATION_ROUND_TRIPS} at the declared link round trip). 16 000 +
420
- * 3 500 = 19 500 ms at the default declaration. The strand formation dial is bounded by this;
478
+ * ({@link relayedDialBudgetMs}, admission allowances included) and then NEGOTIATE one protocol
479
+ * on it ({@link PROTOCOL_NEGOTIATION_ROUND_TRIPS} at the declared link round trip). 21 500 +
480
+ * 3 500 = 25 000 ms at the default declaration. The strand formation dial is bounded by this;
421
481
  * the one-frame control exchanges bound the negotiation together with their request instead
422
482
  * ({@link relayedRequestBudgetMs}).
423
483
  */
@@ -482,9 +542,9 @@ export function circuitRequestBudgetMs(
482
542
 
483
543
  /**
484
544
  * Deadline for opening a (possibly relayed) connection and completing one small request on it:
485
- * {@link relayedDialBudgetMs}, admission allowance included, plus
545
+ * {@link relayedDialBudgetMs}, admission allowances included, plus
486
546
  * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip — so
487
- * {@link RELAYED_REQUEST_ROUND_TRIPS} of link time in all. 16 000 + 2 x 3 500 = 23 000 ms at the
547
+ * {@link RELAYED_REQUEST_ROUND_TRIPS} of link time in all. 21 500 + 2 x 3 500 = 28 500 ms at the
488
548
  * default declaration.
489
549
  *
490
550
  * No transfer allowance, unlike {@link circuitRequestBudgetMs}: the requests this bounds are a few
@@ -581,3 +641,44 @@ export function peerJoinPushBudget(linkRoundTripMs?: number): PeerJoinPushBudget
581
641
  responseTimeoutMs: circuitRequestBudgetMs(linkRoundTripMs)
582
642
  };
583
643
  }
644
+
645
+ /** The connection limits cadre states to Optimystic on every node it builds, beside the declared link. */
646
+ export interface OptimysticDialLimits {
647
+ /** `NodeOptions.connectionManager`: libp2p's limit on one address of a dial, and on a dial that carries no signal. */
648
+ connectionManager: Required<Pick<Libp2pConnectionTimeouts, 'addressDialTimeout' | 'dialTimeout'>>;
649
+ /** `NodeOptions.rpcDeadlines`: the dial deadline of every Optimystic request. */
650
+ rpcDeadlines: Pick<RpcDeadlineDefaults, 'dialTimeoutMs'>;
651
+ }
652
+
653
+ /**
654
+ * The three limits that cover opening a connection, as cadre states them to Optimystic on the
655
+ * control node and every strand node: Optimystic's own derivation from the declared link
656
+ * (`resolveLinkDeadlines`), which is link time only, plus one {@link ADMISSION_DECISION_TIMEOUT_MS}
657
+ * for each of the {@link DIAL_ADMISSION_DECISIONS} gates the open runs through. 39 000 ms per
658
+ * address, 39 000 ms per dial and a 42 500 ms request dial at the default declaration.
659
+ *
660
+ * Built on Optimystic's derivation rather than restating its round-trip counts, so a change to
661
+ * them reaches every node without an edit here. `dialTimeout` stays at least `addressDialTimeout`,
662
+ * as Optimystic's derivation has it, because both take the same addition. The request dial stays
663
+ * one round trip longer than `addressDialTimeout`, also as Optimystic derives them, although
664
+ * Optimystic's `rpcDeadlines` doc asks for it to be no longer: that round trip is the stream
665
+ * negotiation after the connection opens, which the per-address limit does not cover.
666
+ *
667
+ * The listener's `inboundUpgradeTimeout` and the request response deadline are left to Optimystic:
668
+ * the listener's timer runs over its own upgrade and its own decision only (the module doc's
669
+ * "Optimystic's deadlines, from the same declaration"), and a response waits on no gate.
670
+ *
671
+ * The declaration is resolved first, so an invalid one fails with cadre's message. A round trip
672
+ * above Optimystic's `MAX_LINK_ROUND_TRIP_MS` throws from Optimystic, here.
673
+ */
674
+ export function optimysticDialLimits(linkRoundTripMs?: number): OptimysticDialLimits {
675
+ const derived = resolveLinkDeadlines(resolveLinkRoundTripMs(linkRoundTripMs));
676
+ const decisionsMs = DIAL_ADMISSION_DECISIONS * ADMISSION_DECISION_TIMEOUT_MS;
677
+ return {
678
+ connectionManager: {
679
+ addressDialTimeout: derived.addressDialTimeoutMs + decisionsMs,
680
+ dialTimeout: derived.libp2pDialTimeoutMs + decisionsMs
681
+ },
682
+ rpcDeadlines: { dialTimeoutMs: derived.dialTimeoutMs + decisionsMs }
683
+ };
684
+ }
@@ -36,9 +36,9 @@
36
36
  * node has at least one UNEXPIRED, NOT-FULLY-CONSUMED open invitation
37
37
  * outstanding (`StrandSolicitationService.hasOutstandingInvitation` — the
38
38
  * tokens this process minted or published, plus any still-redeemable
39
- * `FormationInvite` row the usage recorder can see). Merely registering the
40
- * responder (`CadreNode.initializeStrandSolicitation`) does NOT suspend it,
41
- * so an app that registers eagerly at node bring-up keeps a live gate. The
39
+ * `FormationInvite` row the usage recorder can see). Registering the
40
+ * responder does NOT suspend it, so every node — each registers one at
41
+ * `CadreNode.start` — keeps a live gate. The
42
42
  * handler's own trust decision remains the per-token check, which is
43
43
  * strictly finer than this one: a peer admitted here can still be rejected
44
44
  * in-protocol for a bogus or spent token.
@@ -5,9 +5,9 @@
5
5
  *
6
6
  * - the trusted-owner anchor (`trusted-owner-store.ts`),
7
7
  * - the cold-start bootstrap-peer store (`bootstrap-peer-store.ts`),
8
- * - the strand peer book (`strand-peer-book.ts`), whose entries are one map per
9
- * strand rather than one value per key — the validation hook below is what
10
- * lets it drop a single junk peer without dropping the strand.
8
+ * - the strand network state (`strand-network-state.ts`): per strand, the state
9
+ * Optimystic's db-p2p saved for the strand node, opaque here past a structural
10
+ * check.
11
11
  *
12
12
  * ANOTHER node-local record — the enrolled-machine count
13
13
  * (`enrolled-machine-store.ts`) — reuses {@link DurableSlot} but deliberately
@@ -72,10 +72,10 @@ const ENVELOPE_VERSION = 1;
72
72
  * in full is not an anchor. Trusting a *subset* of the keys a file claims is
73
73
  * a silent, security-relevant downgrade, so the whole record is discarded
74
74
  * and the node trusts no one until re-seeded out of band.
75
- * - `'drop-entry'` (the bootstrap-peer store and the strand peer book): the
76
- * record is a best-effort dial list and nothing in it is trust-bearing, so one
77
- * junk entry must not discard a stranded node's only remaining way back to
78
- * its party or its strand's peers.
75
+ * - `'drop-entry'` (the bootstrap-peer store and the strand network state):
76
+ * the record is a best-effort dial list and nothing in
77
+ * it is trust-bearing, so one junk entry must not discard a stranded node's
78
+ * only remaining way back to its party or its strand's peers.
79
79
  */
80
80
  export type UnusableEntryPolicy = 'discard-all' | 'drop-entry';
81
81
 
@@ -149,6 +149,11 @@ export class NodeLocalSnapshot<E> {
149
149
  return this.entries.has(key);
150
150
  }
151
151
 
152
+ /** The entry held for `key`, if any. Needs no copy: {@link put} replaces an entry, never mutates it. */
153
+ get(key: string): E | undefined {
154
+ return this.entries.get(key);
155
+ }
156
+
152
157
  /** Fresh copy of the keys — a snapshot decoupled from later {@link put} / {@link remove} calls. */
153
158
  keySnapshot(): Set<string> {
154
159
  return new Set(this.entries.keys());
package/src/peer-dial.ts CHANGED
@@ -2,12 +2,15 @@
2
2
  * Dialing one peer from a list of candidate addresses, so that an address that
3
3
  * never answers cannot use up the time the others needed.
4
4
  *
5
- * libp2p's own multi-address dial (`libp2p.dial(addrs)`, libp2p 3.1.3's
6
- * `DialQueue.dialPeer`) tries the addresses ONE AT A TIME under ONE signal, with
7
- * no time limit per address. An address whose connection attempt is silently
8
- * dropped — a firewall, a subnet the dialer cannot route to, a virtual adapter's
9
- * address a Windows host reports alongside its real ones — holds that dial until
10
- * the whole signal expires, and every address sorted after it is never tried. It
5
+ * libp2p's own multi-address dial (`libp2p.dial(addrs)`, `DialQueue.dialPeer`)
6
+ * tries the addresses ONE AT A TIME under ONE signal. Its only per-address limit
7
+ * is the connection manager's `addressDialTimeout` (libp2p 3.3+), which cadre
8
+ * sizes for a cold relayed open through two admission decisions
9
+ * (`optimysticDialLimits` in `link-budget.ts`: 39 s at the default declared
10
+ * link). An address whose connection attempt is silently dropped — a firewall, a
11
+ * subnet the dialer cannot route to, a virtual adapter's address a Windows host
12
+ * reports alongside its real ones — holds that dial for that long or until the
13
+ * whole signal expires, and the addresses sorted after it get what is left. It
11
14
  * also sorts the list itself (`defaultAddressSorter`), putting loopback addresses
12
15
  * last, behind exactly the LAN addresses most likely to be dropped. A phone
13
16
  * borrowing a node from a cadre-host whose only reachable address was the
@@ -34,17 +37,17 @@ const log = debug('sereus:cadre:peer-dial');
34
37
 
35
38
  /**
36
39
  * Default limit on ONE address's dial attempt, in ms, at the DEFAULT declared link round trip:
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.
40
+ * `RELAYED_DIAL_ROUND_TRIPS` (5) x `DECLARED_LINK_ROUND_TRIP_MS` (3500 ms) plus
41
+ * `DIAL_ADMISSION_DECISIONS` (2) x `ADMISSION_DECISION_TIMEOUT_MS` (2000 ms) = 21 500 ms
42
+ * (`relayedDialBudgetMs`). It was a fixed 8000 ms before it was derived.
40
43
  *
41
44
  * It has to cover the slowest address that should succeed, because an attempt
42
45
  * that needs longer fails the same way on every retry. That is a relayed dial on
43
46
  * a mobile link: connect to the relay (transport, encryption and multiplexer
44
47
  * handshakes, when no relay connection is open yet), open the circuit, then run
45
48
  * both handshakes again end to end through the relay. `link-budget.ts` is where that
46
- * reasoning is now a measured count instead of an estimate — four link round trips, measured at
47
- * 12 094 ms on a link whose round trip is 3 s — so the value MOVES with a host's
49
+ * reasoning is now a measured count instead of an estimate — five link round trips, measured at
50
+ * 15 113 ms on a link whose round trip is 3 s — so the value MOVES with a host's
48
51
  * `NetworkConfig.linkRoundTripMs` rather than pinning the band this node can reach to whatever
49
52
  * number was typed here. A direct dial that works is far quicker — a phone's WebSocket dial to
50
53
  * a node forwarded over `adb reverse` measured 1.6 s end to end.
@@ -200,9 +203,10 @@ export async function dialPeerAddrs(
200
203
  * one address and immediately dialing the next for the same peer therefore
201
204
  * joins the dying job, which rejects the new dial with `AbortError` before it
202
205
  * touches the network — the next address is never tried. Seen against libp2p
203
- * 3.1.3; `test/peer-dial.spec.ts` dials real nodes so an upgrade that changes it
204
- * shows up there. All of the queue's cleanup runs as microtasks, so it has
205
- * finished by the time a macrotask runs.
206
+ * 3.1.3, and the queue's join is unchanged in 3.3.11; `test/peer-dial.spec.ts`
207
+ * dials real nodes so an upgrade that changes it shows up there. All of the
208
+ * queue's cleanup runs as microtasks, so it has finished by the time a
209
+ * macrotask runs.
206
210
  */
207
211
  export async function tryAddrsInTurn<T>(
208
212
  addrs: readonly Multiaddr[],
@@ -148,10 +148,9 @@ export const MAX_BLOCK_MESSAGE_BYTES = 8 * 1024 * 1024;
148
148
  * protocol — the one gate that decides whether {@link PeerJoinBackfill} schedules a peer at
149
149
  * all. A peer that lacks it (a bare circuit relay, a bootstrap node, a stranger the control
150
150
  * network's inbound gate admitted without membership) cannot receive a push no matter how
151
- * long it is dialed, since the protocol id is namespaced per network. Exported because a
152
- * later strand peer-address ticket's identify-driven observation needs the identical test.
151
+ * long it is dialed, since the protocol id is namespaced per network.
153
152
  */
154
- export function speaksBlockTransfer(protocols: readonly string[], protocolPrefix: string): boolean {
153
+ function speaksBlockTransfer(protocols: readonly string[], protocolPrefix: string): boolean {
155
154
  return protocols.includes(buildBlockTransferProtocol(protocolPrefix));
156
155
  }
157
156
 
@@ -176,9 +175,9 @@ export interface PeerJoinBackfillConfig {
176
175
  /** Max blocks per push message. Default 64. */
177
176
  maxChunkBlocks?: number;
178
177
  /**
179
- * Per-push dial deadline, ms. Default {@link peerJoinPushBudget}'s `dialTimeoutMs` — four 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
178
+ * Per-push dial deadline, ms. Default {@link peerJoinPushBudget}'s `dialTimeoutMs` — five link
179
+ * round trips at the declared link plus two admission decisions (a party-run relay's and the
180
+ * called machine's), 21 500 ms as shipped. NOT a fixed number: a relayed dial
182
181
  * costs a fixed number of exchanges, so a host on a slower link moves this (and every other
183
182
  * cadre dial budget) by declaring `NetworkConfig.linkRoundTripMs`. Naming it here still wins
184
183
  * over the derived value — `link-budget.ts` has the counts and the measurement.