@serfab/cadre-core 1.13.0 → 1.15.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 +293 -274
- package/dist/cadre-invite-protocol.d.ts +383 -0
- package/dist/cadre-invite-protocol.js +774 -0
- package/dist/cadre-invite-protocol.js.map +1 -0
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +849 -210
- package/dist/cadre-node.js +1574 -374
- package/dist/cadre-node.js.map +1 -1
- package/dist/claim-proof.d.ts +45 -0
- package/dist/claim-proof.js +132 -0
- package/dist/claim-proof.js.map +1 -0
- package/dist/control-authorization.d.ts +80 -19
- package/dist/control-authorization.js +55 -13
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +605 -108
- package/dist/control-database.js +1299 -280
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +142 -0
- package/dist/control-protocol-guard.js +293 -0
- package/dist/control-protocol-guard.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +817 -1067
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +4 -0
- package/dist/control-stream.js +22 -7
- package/dist/control-stream.js.map +1 -1
- package/dist/delegate-admission.d.ts +1 -1
- package/dist/delegate-admission.js +1 -1
- package/dist/device-token.d.ts +11 -9
- package/dist/device-token.js +14 -12
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +22 -9
- package/dist/ed25519-key.js +47 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +22 -10
- package/dist/index.js +54 -11
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +25 -33
- package/dist/link-budget.js +25 -35
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +191 -151
- package/dist/membership-connection-gater.js +236 -173
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +74 -0
- package/dist/node-claim-payload.js +224 -0
- package/dist/node-claim-payload.js.map +1 -0
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +22 -0
- package/dist/owner-anchor-sync.js +143 -0
- package/dist/owner-anchor-sync.js.map +1 -0
- package/dist/peer-authorization.d.ts +113 -18
- package/dist/peer-authorization.js +225 -29
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -27
- package/dist/peer-dial.js +276 -31
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +26 -2
- package/dist/peer-join-backfill.js +60 -12
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +17 -52
- package/dist/peer-record.js +20 -58
- package/dist/peer-record.js.map +1 -1
- package/dist/pending-join-runner.d.ts +66 -58
- package/dist/pending-join-runner.js +136 -145
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/push-fanout.d.ts +8 -2
- package/dist/push-fanout.js +8 -2
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +165 -123
- package/dist/seed-bootstrap.js +313 -275
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +98 -37
- package/dist/seed-trust-policy.js +188 -2
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +24 -26
- package/dist/strand-addr-protocol.js +19 -26
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +5 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +11 -11
- package/dist/strand-revocation-enforcer.js +11 -11
- package/dist/strand-wake-protocol.d.ts +13 -16
- package/dist/strand-wake-protocol.js +23 -27
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/trusted-owner-store.d.ts +35 -5
- package/dist/trusted-owner-store.js +50 -8
- package/dist/trusted-owner-store.js.map +1 -1
- package/dist/types.d.ts +512 -89
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -10
- package/src/cadre-invite-protocol.ts +1052 -0
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +1703 -386
- package/src/claim-proof.ts +147 -0
- package/src/control-authorization.ts +203 -117
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +4663 -3453
- package/src/control-protocol-guard.ts +348 -0
- package/src/control-schema.ts +828 -1078
- package/src/control-stream.ts +26 -7
- package/src/delegate-admission.ts +1 -1
- package/src/device-token.ts +14 -10
- package/src/ed25519-key.ts +47 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +789 -648
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +25 -36
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +259 -191
- package/src/node-claim-payload.ts +259 -0
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/owner-anchor-sync.ts +171 -0
- package/src/peer-authorization.ts +258 -29
- package/src/peer-dial.ts +604 -315
- package/src/peer-join-backfill.ts +62 -11
- package/src/peer-record.ts +159 -197
- package/src/pending-join-runner.ts +729 -735
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/push-fanout.ts +8 -2
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1448
- package/src/seed-trust-policy.ts +353 -140
- package/src/strand-addr-protocol.ts +26 -35
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-revocation-enforcer.ts +11 -11
- package/src/strand-wake-protocol.ts +23 -30
- package/src/trusted-owner-store.ts +242 -171
- package/src/types.ts +2687 -2242
package/src/peer-dial.ts
CHANGED
|
@@ -1,315 +1,604 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dialing one peer from a list of candidate addresses, so that an address that
|
|
3
|
-
* never answers cannot use up the time the others needed.
|
|
4
|
-
*
|
|
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
|
|
14
|
-
* also sorts the list itself (`defaultAddressSorter`), putting loopback addresses
|
|
15
|
-
* last, behind exactly the LAN addresses most likely to be dropped. A phone
|
|
16
|
-
* borrowing a node from a cadre-host whose only reachable address was the
|
|
17
|
-
* forwarded loopback one never connected for this reason.
|
|
18
|
-
*
|
|
19
|
-
* So the addresses are dialed here, each as its own
|
|
20
|
-
*
|
|
21
|
-
* ({@link PeerDialBudget.totalMs}). One address
|
|
22
|
-
* sorter has nothing to reorder, so the order is
|
|
23
|
-
* ({@link directBeforeRelayed}).
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
*/
|
|
172
|
-
export
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Dialing one peer from a list of candidate addresses, so that an address that
|
|
3
|
+
* never answers cannot use up the time the others needed.
|
|
4
|
+
*
|
|
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
|
|
14
|
+
* also sorts the list itself (`defaultAddressSorter`), putting loopback addresses
|
|
15
|
+
* last, behind exactly the LAN addresses most likely to be dropped. A phone
|
|
16
|
+
* borrowing a node from a cadre-host whose only reachable address was the
|
|
17
|
+
* forwarded loopback one never connected for this reason.
|
|
18
|
+
*
|
|
19
|
+
* So the addresses are dialed here, each as its own transport-manager dial
|
|
20
|
+
* ({@link dialAddr}) with its own time limit ({@link PeerDialBudget.perAddressMs}),
|
|
21
|
+
* inside a limit for the whole peer ({@link PeerDialBudget.totalMs}). One address
|
|
22
|
+
* per call also means libp2p's sorter has nothing to reorder, so the order is
|
|
23
|
+
* chosen here ({@link directBeforeRelayed}). The dial goes around libp2p's dial
|
|
24
|
+
* queue because the queue joins any dial of a peer to a queued job for that peer
|
|
25
|
+
* id, whatever address the new dial names — see {@link dialAddr}. Every address
|
|
26
|
+
* is bound to the peer the caller expects ({@link withTrailingPeerId}) so the
|
|
27
|
+
* handshake verifies who answered.
|
|
28
|
+
*
|
|
29
|
+
* Dependency-free apart from `control-stream.ts` and `link-budget.ts`, so any
|
|
30
|
+
* module may import it without creating an import cycle. That is why the two
|
|
31
|
+
* address helpers ({@link trailingPeerId}, {@link withTrailingPeerId}) live here
|
|
32
|
+
* rather than in `peer-record.ts`, which pulls in the crypto plugin.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import debug from 'debug';
|
|
36
|
+
import type { Connection, ConnectionGater, Libp2pStatus, PeerId } from '@libp2p/interface';
|
|
37
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
38
|
+
import type { Component, Multiaddr } from '@multiformats/multiaddr';
|
|
39
|
+
import { withDeadline } from './control-stream.js';
|
|
40
|
+
import { relayedDialBudgetMs } from './link-budget.js';
|
|
41
|
+
|
|
42
|
+
const log = debug('sereus:cadre:peer-dial');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The peer id a multiaddr terminates in, or `null` when it names none.
|
|
46
|
+
*
|
|
47
|
+
* The LAST `/p2p/` component is the dial target: a circuit address
|
|
48
|
+
* (`…/p2p/<relay>/p2p-circuit/p2p/<target>`) names the relay first and the peer
|
|
49
|
+
* being reached last. Replaces the deprecated `Multiaddr.getPeerId()`, which
|
|
50
|
+
* cannot express that distinction.
|
|
51
|
+
*/
|
|
52
|
+
export function trailingPeerId(addr: Multiaddr): string | null {
|
|
53
|
+
const p2p = addr.getComponents().filter((c) => c.name === 'p2p');
|
|
54
|
+
return p2p.length > 0 ? (p2p[p2p.length - 1].value ?? null) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `addr` guaranteed to terminate in `/p2p/<peerId>` — the shape every dial needs,
|
|
59
|
+
* since the upgrader verifies that id during the handshake — or `null` when it
|
|
60
|
+
* cannot, because the address already terminates in a DIFFERENT peer id and
|
|
61
|
+
* therefore does not reach `peerId` at all.
|
|
62
|
+
*
|
|
63
|
+
* Three input shapes, matching what a `CadrePeer` record can legitimately carry:
|
|
64
|
+
*
|
|
65
|
+
* - no `/p2p/` component (`/ip4/1.2.3.4/tcp/4001`) — the suffix is appended;
|
|
66
|
+
* - a relay hop with the destination missing (`…/p2p/<relay>/p2p-circuit`) — the
|
|
67
|
+
* last component is the circuit, not a peer id, so the suffix is appended and
|
|
68
|
+
* the address becomes the full `…/p2p/<relay>/p2p-circuit/p2p/<peerId>`;
|
|
69
|
+
* - already terminating in `/p2p/<X>` — returned untouched when `X` is `peerId`,
|
|
70
|
+
* dropped otherwise.
|
|
71
|
+
*
|
|
72
|
+
* This is deliberately the same rule libp2p applies internally in
|
|
73
|
+
* `calculateMultiaddrs` (append when the LAST component is not `p2p`) followed by
|
|
74
|
+
* its wrong-peer-id filter, so a normalized list matches what libp2p would have
|
|
75
|
+
* built anyway. Normalizing before a list dial is what keeps a list from mixing
|
|
76
|
+
* suffixed and unsuffixed entries, which `getPeerAddress` rejects outright
|
|
77
|
+
* ("Multiaddrs must all have the same peer id or have no peer id") — taking the
|
|
78
|
+
* whole peer down with it, not just the odd address.
|
|
79
|
+
*
|
|
80
|
+
* Throws when `peerId` does not parse as a peer id (the encapsulation validates
|
|
81
|
+
* it). Never use before signature verification: verification runs against the
|
|
82
|
+
* original on-record strings.
|
|
83
|
+
*/
|
|
84
|
+
export function withTrailingPeerId(addr: Multiaddr, peerId: string): Multiaddr | null {
|
|
85
|
+
const components = addr.getComponents();
|
|
86
|
+
if (components[components.length - 1]?.name !== 'p2p') {
|
|
87
|
+
return addr.encapsulate(`/p2p/${peerId}`);
|
|
88
|
+
}
|
|
89
|
+
return trailingPeerId(addr) === peerId ? addr : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default limit on ONE address's dial attempt, in ms, at the DEFAULT declared link round trip:
|
|
94
|
+
* `RELAYED_DIAL_ROUND_TRIPS` (5) x `DECLARED_LINK_ROUND_TRIP_MS` (3500 ms) plus
|
|
95
|
+
* `DIAL_ADMISSION_DECISIONS` (2) x `ADMISSION_DECISION_TIMEOUT_MS` (2000 ms) = 21 500 ms
|
|
96
|
+
* (`relayedDialBudgetMs`). It was a fixed 8000 ms before it was derived.
|
|
97
|
+
*
|
|
98
|
+
* It has to cover the slowest address that should succeed, because an attempt
|
|
99
|
+
* that needs longer fails the same way on every retry. That is a relayed dial on
|
|
100
|
+
* a mobile link: connect to the relay (transport, encryption and multiplexer
|
|
101
|
+
* handshakes, when no relay connection is open yet), open the circuit, then run
|
|
102
|
+
* both handshakes again end to end through the relay. `link-budget.ts` is where that
|
|
103
|
+
* reasoning is now a measured count instead of an estimate — five link round trips, measured at
|
|
104
|
+
* 15 113 ms on a link whose round trip is 3 s — so the value MOVES with a host's
|
|
105
|
+
* `NetworkConfig.linkRoundTripMs` rather than pinning the band this node can reach to whatever
|
|
106
|
+
* number was typed here. A direct dial that works is far quicker — a phone's WebSocket dial to
|
|
107
|
+
* a node forwarded over `adb reverse` measured 1.6 s end to end.
|
|
108
|
+
*
|
|
109
|
+
* Deriving it rather than leaving it fixed is deliberate: at 8000 ms fixed, the per-address
|
|
110
|
+
* limit — not libp2p's own 10 s dial timeout — was the first thing to cut a relayed dial off,
|
|
111
|
+
* from about 1000 ms of one-way link delay upward. Nothing said so, and a reader raising
|
|
112
|
+
* `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` below would not have found it.
|
|
113
|
+
*
|
|
114
|
+
* NOTE: a `/p2p-circuit/webrtc` address also negotiates a WebRTC session after
|
|
115
|
+
* the relay leg. If such dials are ever seen failing at exactly this limit, raise
|
|
116
|
+
* the declared link round trip (or `network.controlCohort.perAddressDialTimeoutMs` on the
|
|
117
|
+
* affected nodes) rather than the per-peer limit, which cannot help an address that is itself
|
|
118
|
+
* cut off.
|
|
119
|
+
*
|
|
120
|
+
* Override per node with `network.controlCohort.perAddressDialTimeoutMs`.
|
|
121
|
+
*/
|
|
122
|
+
export const DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS = relayedDialBudgetMs();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How many whole per-address dial attempts {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS}
|
|
126
|
+
* budgets for: the address that works plus the three dead ones a lent cadre-host node is
|
|
127
|
+
* observed to report ahead of it (loopback and LAN WebSocket addresses, plus virtual adapters).
|
|
128
|
+
*/
|
|
129
|
+
export const CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS = 4;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Default limit on dialing ONE peer — every candidate address together — in ms.
|
|
133
|
+
*
|
|
134
|
+
* Without it, an offline peer costs (address count × up to
|
|
135
|
+
* {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}), a number that
|
|
136
|
+
* depends on how many addresses the peer happens to report. Callers dial peers
|
|
137
|
+
* one after another (the control-cohort reconcile pass dials each sibling in
|
|
138
|
+
* turn), so that cost also delays every later peer. With the limit, a reconcile
|
|
139
|
+
* pass costs at most (dialed siblings) × this.
|
|
140
|
+
*
|
|
141
|
+
* Sized to fit a typical list with dead addresses ahead of the one that works:
|
|
142
|
+
* a lent cadre-host node reports two to four addresses a phone can dial
|
|
143
|
+
* (loopback and LAN WebSocket addresses, plus any virtual adapters). So it is
|
|
144
|
+
* {@link CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS} whole per-address budgets — three silently
|
|
145
|
+
* dropped addresses, then the one that works, each getting the FULL per-address limit. A peer
|
|
146
|
+
* whose working address sits behind four or more dropped ones does not fit, and fails each pass
|
|
147
|
+
* the same way; if that shows up, the remedy is ordering (try the address that last connected
|
|
148
|
+
* first), not a larger number here.
|
|
149
|
+
*
|
|
150
|
+
* Counted as whole per-address budgets rather than the flat 30_000 it was before, because the
|
|
151
|
+
* old number left only 6 s after three dropped addresses — enough for the 1.6 s direct dial the
|
|
152
|
+
* phone measured, but NOT for the relayed dial the per-address limit is sized to cover. A peer
|
|
153
|
+
* whose only working address was a relayed one behind two dead ones therefore could not be
|
|
154
|
+
* dialled at all, and the arithmetic in this comment was what said otherwise.
|
|
155
|
+
*
|
|
156
|
+
* Override per node with `network.controlCohort.dialTimeoutMs` — tests that
|
|
157
|
+
* drive dead addresses on purpose set it low so a pass's duration is a chosen
|
|
158
|
+
* number rather than a transitive libp2p default stretched by machine load.
|
|
159
|
+
*/
|
|
160
|
+
export const DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS =
|
|
161
|
+
CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS * DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS;
|
|
162
|
+
|
|
163
|
+
/** Time limits for dialing one peer from a list of candidate addresses. */
|
|
164
|
+
export interface PeerDialBudget {
|
|
165
|
+
/** Limit on one address's attempt, in ms. */
|
|
166
|
+
perAddressMs: number;
|
|
167
|
+
/** Limit on the whole peer — every address together — in ms. */
|
|
168
|
+
totalMs: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** {@link PeerDialBudget} built from the two defaults above. */
|
|
172
|
+
export const DEFAULT_PEER_DIAL_BUDGET: Readonly<PeerDialBudget> = {
|
|
173
|
+
perAddressMs: DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS,
|
|
174
|
+
totalMs: DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** What {@link dialAddr} reads of a connection the node holds, to decide whether to hand it back instead of dialing. */
|
|
178
|
+
export interface ReusableConnection {
|
|
179
|
+
remotePeer: { toString(): string };
|
|
180
|
+
status?: Connection['status'];
|
|
181
|
+
limits?: Connection['limits'];
|
|
182
|
+
/** Whether the connection is direct rather than relayed; absent counts as direct. */
|
|
183
|
+
direct?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The slice of a libp2p node {@link dialPeerAddrs} uses. */
|
|
187
|
+
export interface AddrDialer {
|
|
188
|
+
/** This node's id, to recognise an address that relays through this node ({@link relaysThrough}). */
|
|
189
|
+
readonly peerId: PeerId;
|
|
190
|
+
/** libp2p's lifecycle state: {@link dialAddr} refuses to start an attempt once it is not `started`. */
|
|
191
|
+
readonly status?: Libp2pStatus;
|
|
192
|
+
/**
|
|
193
|
+
* The connections this node holds to `peerId`. {@link dialAddr} returns an open one without relay
|
|
194
|
+
* limits rather than opening a second — what libp2p's dial queue did (`findExistingConnection`)
|
|
195
|
+
* and the transport manager does not. Optional so a double need not provide it, and typed by
|
|
196
|
+
* what is read of each connection so the strand redial's node slice (bare `remotePeer`s, which
|
|
197
|
+
* never count as open) satisfies it without a cast.
|
|
198
|
+
*/
|
|
199
|
+
getConnections?(peerId?: PeerId): ReadonlyArray<ReusableConnection>;
|
|
200
|
+
dial(addr: Multiaddr, options: { signal: AbortSignal }): Promise<Connection>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The libp2p internals a queue-free dial reaches — real fields on libp2p's node class, absent
|
|
205
|
+
* from the `Libp2p` interface (the seam `relay-reservation.ts` already uses).
|
|
206
|
+
*/
|
|
207
|
+
interface DialComponentsLike {
|
|
208
|
+
components?: {
|
|
209
|
+
transportManager?: { dial?: (addr: Multiaddr, options: { signal: AbortSignal }) => Promise<Connection> };
|
|
210
|
+
connectionGater?: ConnectionGater;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* libp2p's `stop` event, which ends an attempt the dial queue would otherwise have aborted at
|
|
216
|
+
* shutdown. Structural so an {@link AddrDialer} double need not provide it.
|
|
217
|
+
*/
|
|
218
|
+
interface StopEventsLike {
|
|
219
|
+
addEventListener?: (type: 'stop', listener: () => void) => void;
|
|
220
|
+
removeEventListener?: (type: 'stop', listener: () => void) => void;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Dial ONE address outside libp2p's dial queue. The queue joins any new dial to a queued job
|
|
225
|
+
* for the same peer id, whatever address the new dial names, and a joined job ends only when
|
|
226
|
+
* every caller has aborted — so while another caller dials this peer (FRET's membership probe,
|
|
227
|
+
* an Optimystic cluster client), a per-address attempt through the queue waits on whatever
|
|
228
|
+
* address that job is on instead of its own. The transport manager dials exactly the address
|
|
229
|
+
* given; the two gater hooks the queue would have consulted are run here, and the upgrader
|
|
230
|
+
* still runs `denyOutboundConnection` and verifies the peer id the address carries during the
|
|
231
|
+
* Noise handshake, so the address must carry the id of the peer expected to answer
|
|
232
|
+
* ({@link withTrailingPeerId}). An open connection to that peer without relay limits is handed
|
|
233
|
+
* back instead of dialing ({@link reusableConnection}), as the queue would have. A relayed
|
|
234
|
+
* address still passes through the queue for its relay leg (the circuit transport dials the
|
|
235
|
+
* relay by peer id).
|
|
236
|
+
*
|
|
237
|
+
* The queue would also have aborted the attempt at shutdown; here the node's `status` is
|
|
238
|
+
* checked before dialing and its `stop` event aborts an attempt in flight. Without
|
|
239
|
+
* `transportManager.dial` (libp2p internals moved) the attempt goes through the queue, logged.
|
|
240
|
+
*
|
|
241
|
+
* What else the queue did that this does not: record the outcome in the peer store
|
|
242
|
+
* (`LAST_DIAL_SUCCESS_KEY` / `LAST_DIAL_FAILURE_KEY`, read only by libp2p's own address sorter,
|
|
243
|
+
* which these dials do not use); apply `maxParallelDials` and `maxDialQueueLength`; and refuse
|
|
244
|
+
* a dial of this node's own id before touching the network — the upgrader still throws
|
|
245
|
+
* `Can not dial self` after the handshake, `dialPeerAddrs` drops addresses relaying through this
|
|
246
|
+
* node, `applySeed` skips itself, and the reconcile pass never lists itself.
|
|
247
|
+
*/
|
|
248
|
+
export async function dialAddr(dialer: AddrDialer, addr: Multiaddr, signal: AbortSignal, label: string): Promise<Connection> {
|
|
249
|
+
if (dialer.status !== undefined && dialer.status !== 'started') {
|
|
250
|
+
throw new Error(`${label}: not dialed, the node is ${dialer.status}`);
|
|
251
|
+
}
|
|
252
|
+
const components = (dialer as AddrDialer & DialComponentsLike).components;
|
|
253
|
+
const manager = components?.transportManager;
|
|
254
|
+
if (typeof manager?.dial !== 'function') {
|
|
255
|
+
log('%s: no transportManager.dial on this node — libp2p internals moved? dialing through the queue', label);
|
|
256
|
+
return dialer.dial(addr, { signal });
|
|
257
|
+
}
|
|
258
|
+
const target = boundPeer(addr, label);
|
|
259
|
+
const open = reusableConnection(dialer, target, addr);
|
|
260
|
+
if (open !== undefined) {
|
|
261
|
+
log('%s: reusing the open connection to %s', label, target);
|
|
262
|
+
return open;
|
|
263
|
+
}
|
|
264
|
+
const attempt = attemptSignal(dialer, signal, label);
|
|
265
|
+
try {
|
|
266
|
+
await assertDialAllowed(components?.connectionGater, target, addr, label);
|
|
267
|
+
if (attempt.signal.aborted) {
|
|
268
|
+
throw attempt.signal.reason;
|
|
269
|
+
}
|
|
270
|
+
// NOTE: a relayed address's relay leg still goes through the dial queue (the circuit
|
|
271
|
+
// transport dials the relay by peer id), so another dial of the relay can join it. Fine
|
|
272
|
+
// while a relay connection is normally already open for the reservation; revisit if a
|
|
273
|
+
// relayed attempt is ever seen timing out while a dial of the relay by id is in flight.
|
|
274
|
+
return await manager.dial(addr, { signal: attempt.signal });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
// A transport reports an aborted connect as its own failure ("Could not connect to …");
|
|
277
|
+
// why it was aborted — the deadline, or the stop — is the error worth surfacing.
|
|
278
|
+
throw attempt.signal.aborted ? attempt.signal.reason : err;
|
|
279
|
+
} finally {
|
|
280
|
+
attempt.release();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The signal one attempt dials under: aborted when the caller's `signal` aborts, with its
|
|
286
|
+
* reason, or when the node stops. Linked before anything in the attempt awaits (the gater
|
|
287
|
+
* hooks are async), so neither event can fall between the check and the dial; a `signal`
|
|
288
|
+
* already aborted on entry aborts it at once, since no `abort` event will come. `release`
|
|
289
|
+
* unhooks both listeners.
|
|
290
|
+
*/
|
|
291
|
+
function attemptSignal(dialer: AddrDialer, signal: AbortSignal, label: string): { signal: AbortSignal; release(): void } {
|
|
292
|
+
const events = dialer as AddrDialer & StopEventsLike;
|
|
293
|
+
const controller = new AbortController();
|
|
294
|
+
const onAbort = (): void => controller.abort(signal.reason);
|
|
295
|
+
const onStop = (): void => controller.abort(new Error(`${label}: the node stopped`));
|
|
296
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
297
|
+
events.addEventListener?.('stop', onStop);
|
|
298
|
+
if (signal.aborted) {
|
|
299
|
+
onAbort();
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
signal: controller.signal,
|
|
303
|
+
release: () => {
|
|
304
|
+
signal.removeEventListener('abort', onAbort);
|
|
305
|
+
events.removeEventListener?.('stop', onStop);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The peer `addr` is bound to, or `undefined` when it names none. Throws, naming the address,
|
|
312
|
+
* when the id it carries does not parse: the upgrader would reject the same address after
|
|
313
|
+
* connecting, so failing before the network costs nothing and says why.
|
|
314
|
+
*/
|
|
315
|
+
function boundPeer(addr: Multiaddr, label: string): PeerId | undefined {
|
|
316
|
+
const id = trailingPeerId(addr);
|
|
317
|
+
if (id === null) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return peerIdFromString(id);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
throw new Error(`${label}: ${addr.toString()} does not end in a peer id that parses`, { cause: err });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* An open connection to `target` without relay limits, when the node holds one — the rule
|
|
329
|
+
* libp2p's dial queue applied before dialing (`findExistingConnection`): a direct connection is
|
|
330
|
+
* preferred, and a relayed one is not reused for a direct address, so the dial can upgrade the
|
|
331
|
+
* link. `undefined` when the node holds none, or does not expose its connections.
|
|
332
|
+
*
|
|
333
|
+
* Returned as the `Connection` it is on a real node. A double that reports an open connection
|
|
334
|
+
* must supply what its caller uses of one.
|
|
335
|
+
*/
|
|
336
|
+
function reusableConnection(dialer: AddrDialer, target: PeerId | undefined, addr: Multiaddr): Connection | undefined {
|
|
337
|
+
if (target === undefined || dialer.getConnections === undefined) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
const expected = target.toString();
|
|
341
|
+
const open = dialer.getConnections(target)
|
|
342
|
+
.filter((c) => c.status === 'open' && c.limits === undefined && c.remotePeer.toString() === expected);
|
|
343
|
+
const existing = open.find((c) => c.direct !== false) ?? open[0];
|
|
344
|
+
if (existing === undefined || (existing.direct === false && !isRelayed(addr))) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
return existing as Connection;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function assertDialAllowed(gater: ConnectionGater | undefined, target: PeerId | undefined, addr: Multiaddr, label: string): Promise<void> {
|
|
351
|
+
if (target !== undefined && await gater?.denyDialPeer?.(target)) {
|
|
352
|
+
throw new Error(`${label}: the connection gater denies dialing ${target.toString()}`);
|
|
353
|
+
}
|
|
354
|
+
if (await gater?.denyDialMultiaddr?.(addr)) {
|
|
355
|
+
throw new Error(`${label}: the connection gater denies dialing ${addr.toString()}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Thrown by {@link dialPeerAddrs} when it was given addresses but every one reaches the peer by
|
|
361
|
+
* relaying through this node, so none was dialed.
|
|
362
|
+
*
|
|
363
|
+
* This is where a relay lands for a peer that holds a reservation on it (a phone, or any node that
|
|
364
|
+
* cannot listen): the address that peer publishes is `/<relay transport>/p2p/<relay>/p2p-circuit/p2p/<peer>`,
|
|
365
|
+
* and dialing it from the relay itself fails with libp2p's `Can not dial self`. Nothing this node
|
|
366
|
+
* does can repair it; once the peer's connection drops, only the peer can reconnect. Callers log it
|
|
367
|
+
* as that rather than as a dial failure.
|
|
368
|
+
*/
|
|
369
|
+
export class SelfRelayOnlyError extends Error {
|
|
370
|
+
constructor(label: string) {
|
|
371
|
+
super(`${label}: every candidate address relays through this node; only the peer can reconnect`);
|
|
372
|
+
this.name = 'SelfRelayOnlyError';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* No connection to the peer formed, so nothing was sent to it. `cause` is what the dial threw:
|
|
378
|
+
* {@link dialPeerAddrs}'s error naming every address and why it failed, or a
|
|
379
|
+
* {@link SelfRelayOnlyError}.
|
|
380
|
+
*
|
|
381
|
+
* Kept apart from failures after a connection formed so a caller can tell "this device could not
|
|
382
|
+
* reach the peer" from "the peer was reached and then something else failed" without reading
|
|
383
|
+
* libp2p's error text. `SeedBootstrapService.deliverSeed` throws it, and so `CadreNode.claimNode`.
|
|
384
|
+
*/
|
|
385
|
+
export class PeerUnreachableError extends Error {
|
|
386
|
+
constructor(readonly peerId: string, cause: unknown) {
|
|
387
|
+
super(`Could not reach ${peerId}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
388
|
+
this.name = 'PeerUnreachableError';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One candidate's outcome, kept so the thrown error can name every attempt. */
|
|
393
|
+
interface AddrAttemptFailure {
|
|
394
|
+
addr: Multiaddr;
|
|
395
|
+
error: Error;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Dial `peerId` from `addrs`, one address per {@link dialAddr} call, and return the
|
|
400
|
+
* first connection that forms. Direct addresses are tried before relayed ones
|
|
401
|
+
* ({@link directBeforeRelayed}); otherwise the given order is kept.
|
|
402
|
+
*
|
|
403
|
+
* Every address is first bound to `peerId` ({@link withTrailingPeerId}), so the
|
|
404
|
+
* handshake verifies that the peer expected is the one that answered; an address
|
|
405
|
+
* naming another peer is dropped and logged. When that leaves nothing of a
|
|
406
|
+
* non-empty list — every address names someone else, or `peerId` does not parse —
|
|
407
|
+
* this throws without dialing.
|
|
408
|
+
*
|
|
409
|
+
* Addresses that relay through this node are dropped next ({@link relaysThrough}):
|
|
410
|
+
* dialing one could only fail with `Can not dial self`. When that leaves nothing,
|
|
411
|
+
* this throws {@link SelfRelayOnlyError} without dialing.
|
|
412
|
+
*
|
|
413
|
+
* Addresses naming a transport this node lacks are not filtered out first: the
|
|
414
|
+
* transport manager rejects those at once (`TransportUnavailableError`), so they
|
|
415
|
+
* cost a log line, not time.
|
|
416
|
+
*
|
|
417
|
+
* Otherwise throws when no address connects — see {@link tryAddrsInTurn} for the error.
|
|
418
|
+
*
|
|
419
|
+
* @param peerId The peer every address must reach, as every caller already holds it
|
|
420
|
+
* (a `CadrePeer` row key, a seed peer's id, a strand member's id).
|
|
421
|
+
* @param label Names the dial in timeout messages and logs, e.g.
|
|
422
|
+
* `reconcileControlCohort dial of sibling <peerId>`; each attempt reads
|
|
423
|
+
* `<label> via <addr>`.
|
|
424
|
+
*/
|
|
425
|
+
export async function dialPeerAddrs(
|
|
426
|
+
dialer: AddrDialer,
|
|
427
|
+
peerId: string,
|
|
428
|
+
addrs: readonly Multiaddr[],
|
|
429
|
+
budget: PeerDialBudget,
|
|
430
|
+
label: string,
|
|
431
|
+
): Promise<Connection> {
|
|
432
|
+
const bound = boundTo(peerId, addrs, label);
|
|
433
|
+
if (addrs.length > 0 && bound.length === 0) {
|
|
434
|
+
throw new Error(`${label}: none of the ${addrs.length} candidate addresses binds to ${peerId}`);
|
|
435
|
+
}
|
|
436
|
+
const dialable = withoutSelfRelayed(dialer, bound, label);
|
|
437
|
+
if (bound.length > 0 && dialable.length === 0) {
|
|
438
|
+
throw new SelfRelayOnlyError(label);
|
|
439
|
+
}
|
|
440
|
+
return tryAddrsInTurn(directBeforeRelayed(dialable), budget, label, (addr, signal) => dialAddr(dialer, addr, signal, label));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* `addrs` each bound to `peerId` ({@link withTrailingPeerId}). An address naming another peer
|
|
445
|
+
* is dropped, as is every address when `peerId` cannot be encapsulated (it does not parse);
|
|
446
|
+
* each drop is logged with its reason.
|
|
447
|
+
*/
|
|
448
|
+
function boundTo(peerId: string, addrs: readonly Multiaddr[], label: string): Multiaddr[] {
|
|
449
|
+
const bound: Multiaddr[] = [];
|
|
450
|
+
for (const addr of addrs) {
|
|
451
|
+
try {
|
|
452
|
+
const withId = withTrailingPeerId(addr, peerId);
|
|
453
|
+
if (withId === null) {
|
|
454
|
+
log('%s: dropping %s — it names a peer other than %s', label, addr.toString(), peerId);
|
|
455
|
+
} else {
|
|
456
|
+
bound.push(withId);
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
log('%s: cannot bind %s to %s: %o', label, addr.toString(), peerId, err);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return bound;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Run `attempt` against each address in the given order until one succeeds, and
|
|
467
|
+
* return its result.
|
|
468
|
+
*
|
|
469
|
+
* Each attempt is limited to `budget.perAddressMs`, or to whatever remains of
|
|
470
|
+
* `budget.totalMs` when that is less. The attempt's signal is aborted when its
|
|
471
|
+
* limit passes, so a dial in progress is cancelled rather than left running.
|
|
472
|
+
* Addresses the total leaves no time for are reported as not tried.
|
|
473
|
+
*
|
|
474
|
+
* Throws when every address fails. With one address the error is that attempt's
|
|
475
|
+
* own, unchanged. With several it is one error naming each address and why it
|
|
476
|
+
* failed, in order, with the first failure as `cause` — so the report shows the
|
|
477
|
+
* address that mattered, not only whichever happened to be tried last.
|
|
478
|
+
*
|
|
479
|
+
* Between attempts this waits for one macrotask (`setTimeout(0)`). A direct
|
|
480
|
+
* address is dialed outside libp2p's dial queue ({@link dialAddr}), but a relayed
|
|
481
|
+
* address still passes through it for the relay leg, and the queue lets a new
|
|
482
|
+
* dial join any queued job for the same peer id WITHOUT checking whether that
|
|
483
|
+
* job has already finished: an aborted job leaves the queue a few microtasks
|
|
484
|
+
* after its callers are rejected, so a next attempt through the same relay made
|
|
485
|
+
* at once would join the dying job and be rejected with `AbortError` before it
|
|
486
|
+
* touches the network. All of the queue's cleanup runs as microtasks, so it has
|
|
487
|
+
* finished by the time a macrotask runs. Seen against libp2p 3.1.3, and the
|
|
488
|
+
* queue's join is unchanged in 3.3.11; `test/peer-dial.spec.ts` dials real nodes
|
|
489
|
+
* so an upgrade that changes it shows up there.
|
|
490
|
+
*/
|
|
491
|
+
export async function tryAddrsInTurn<T>(
|
|
492
|
+
addrs: readonly Multiaddr[],
|
|
493
|
+
budget: PeerDialBudget,
|
|
494
|
+
label: string,
|
|
495
|
+
attempt: (addr: Multiaddr, signal: AbortSignal, attemptMs: number) => Promise<T>,
|
|
496
|
+
): Promise<T> {
|
|
497
|
+
const deadline = Date.now() + budget.totalMs;
|
|
498
|
+
const failures: AddrAttemptFailure[] = [];
|
|
499
|
+
for (const addr of addrs) {
|
|
500
|
+
const remaining = deadline - Date.now();
|
|
501
|
+
if (remaining <= 0) {
|
|
502
|
+
failures.push({ addr, error: new Error(`not tried — the ${budget.totalMs}ms budget for ${label} was spent on earlier addresses`) });
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (failures.length > 0) {
|
|
506
|
+
await nextMacrotask();
|
|
507
|
+
}
|
|
508
|
+
const attemptMs = Math.min(budget.perAddressMs, remaining);
|
|
509
|
+
try {
|
|
510
|
+
return await withDeadline(attemptMs, `${label} via ${addr.toString()}`, (signal) => attempt(addr, signal, attemptMs));
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
513
|
+
failures.push({ addr, error });
|
|
514
|
+
log('%s via %s failed: %o', label, addr.toString(), error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
throw allAttemptsFailed(label, failures);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* `addrs` with every circuit-relay address (one containing `/p2p-circuit`) moved
|
|
522
|
+
* after every direct one, each group keeping its given order.
|
|
523
|
+
*
|
|
524
|
+
* The one part of libp2p's own ordering worth keeping: a relayed connection is
|
|
525
|
+
* slower and limited in how much data and time it may carry, so a direct
|
|
526
|
+
* address is always worth trying first. libp2p's other rules are deliberately
|
|
527
|
+
* dropped — it puts loopback addresses last and public ones before private ones,
|
|
528
|
+
* which is how a forwarded loopback address ended up behind addresses that
|
|
529
|
+
* never answer. With a per-address limit, trying a dead address first costs a
|
|
530
|
+
* bounded amount of time, so the caller's order is the better guide.
|
|
531
|
+
*/
|
|
532
|
+
export function directBeforeRelayed(addrs: readonly Multiaddr[]): Multiaddr[] {
|
|
533
|
+
return [...addrs.filter((addr) => !isRelayed(addr)), ...addrs.filter(isRelayed)];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Whether `addr` reaches its target through a circuit relay. */
|
|
537
|
+
function isRelayed(addr: Multiaddr): boolean {
|
|
538
|
+
return addr.getComponents().some((c) => c.name === 'p2p-circuit');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** `addrs` without those that relay through `dialer` itself, each dropped one logged. */
|
|
542
|
+
function withoutSelfRelayed(dialer: AddrDialer, addrs: readonly Multiaddr[], label: string): Multiaddr[] {
|
|
543
|
+
return addrs.filter((addr) => {
|
|
544
|
+
if (!relaysThrough(addr, dialer.peerId)) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
log('%s: skipping %s — it relays through this node', label, addr.toString());
|
|
548
|
+
return false;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Whether `addr` reaches its target by relaying through `relay` at any hop.
|
|
554
|
+
*
|
|
555
|
+
* A circuit address names its relay in the `p2p` component immediately before
|
|
556
|
+
* the `p2p-circuit` marker (`/<transport>/p2p/<relay>/p2p-circuit/p2p/<target>`),
|
|
557
|
+
* so the components are walked rather than the text searched: the target's id
|
|
558
|
+
* after the marker, a bare `/p2p-circuit` naming no relay, and a multi-hop chain
|
|
559
|
+
* each read correctly that way. Same rule as db-p2p's `routesThroughRelay`, which
|
|
560
|
+
* its package does not export.
|
|
561
|
+
*/
|
|
562
|
+
function relaysThrough(addr: Multiaddr, relay: PeerId): boolean {
|
|
563
|
+
const components = addr.getComponents();
|
|
564
|
+
return components.some((component, i) => component.name === 'p2p-circuit' && namesPeer(components[i - 1], relay));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Whether `component` is a `p2p` component naming `peer`. Compared as canonical
|
|
569
|
+
* strings because a multiaddr keeps a peer id in whichever form it was written
|
|
570
|
+
* (base58 or CIDv1). `PeerId.equals` is avoided because test doubles supply the
|
|
571
|
+
* dialer's `peerId` with `toString` only.
|
|
572
|
+
*/
|
|
573
|
+
function namesPeer(component: Component | undefined, peer: PeerId): boolean {
|
|
574
|
+
if (component?.name !== 'p2p' || component.value === undefined) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const expected = peer.toString();
|
|
578
|
+
try {
|
|
579
|
+
return peerIdFromString(component.value).toString() === expected;
|
|
580
|
+
} catch (err) {
|
|
581
|
+
log('cannot read %s as a peer id; treating it as not this node: %o', component.value, err);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function allAttemptsFailed(label: string, failures: AddrAttemptFailure[]): Error {
|
|
587
|
+
if (failures.length === 0) {
|
|
588
|
+
return new Error(`${label}: no candidate addresses`);
|
|
589
|
+
}
|
|
590
|
+
if (failures.length === 1) {
|
|
591
|
+
return failures[0].error;
|
|
592
|
+
}
|
|
593
|
+
const detail = failures.map((f) => `${f.addr.toString()} — ${f.error.message}`).join('; ');
|
|
594
|
+
return new Error(`${label} failed for all ${failures.length} candidate addresses: ${detail}`, { cause: failures[0].error });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Yield one macrotask: libp2p's dial queue drops an aborted job only on the next turn, so a
|
|
599
|
+
* dialer that moves on to another address after an abort waits this long first. Shared with
|
|
600
|
+
* the cadre invitation dialer (`cadre-invite-protocol.ts`).
|
|
601
|
+
*/
|
|
602
|
+
export function nextMacrotask(): Promise<void> {
|
|
603
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
604
|
+
}
|