@optimystic/db-p2p 0.24.0 → 0.24.2
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/dist/src/cluster/service.d.ts +8 -0
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +16 -4
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +25 -11
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +31 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +68 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +123 -14
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +8 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +2 -2
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +69 -0
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +110 -15
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/service.d.ts +6 -0
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +12 -2
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
- package/dist/src/routing/libp2p-known-peers.js +5 -0
- package/dist/src/routing/libp2p-known-peers.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/package.json +3 -3
- package/src/cluster/service.ts +305 -293
- package/src/cohort-topic/host.ts +2932 -2901
- package/src/cohort-topic/stream-util.ts +147 -135
- package/src/libp2p-key-network.ts +1235 -1120
- package/src/libp2p-node-base.ts +1678 -1675
- package/src/logger.ts +27 -27
- package/src/matchmaking/query-transport.ts +492 -492
- package/src/peer-address-book.ts +266 -149
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +412 -408
- package/src/repo/service.ts +323 -313
- package/src/routing/libp2p-known-peers.ts +31 -26
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
package/src/peer-address-book.ts
CHANGED
|
@@ -1,149 +1,266 @@
|
|
|
1
|
-
import type { PeerId } from '@libp2p/interface'
|
|
2
|
-
import { peerIdFromString } from '@libp2p/peer-id'
|
|
3
|
-
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Cap on addresses merged per peer from one application-level message.
|
|
7
|
-
*
|
|
8
|
-
* Without a cap, a crafted cluster record or redirect payload could stuff the address
|
|
9
|
-
* book and turn every cohort member into a dial amplifier aimed at an address of the
|
|
10
|
-
* sender's choosing. This bounds the per-peer cost;
|
|
11
|
-
* {@link MAX_LEARNED_PEERS_PER_RECORD} bounds how many peers one record may introduce.
|
|
12
|
-
*/
|
|
13
|
-
export const MAX_MERGED_ADDRS_PER_PEER = 8
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Cap on how many distinct peers one cluster record may teach us addresses for.
|
|
17
|
-
*
|
|
18
|
-
* A record's peer map is NOT self-limiting: `ClusterService.processOperation` learns from it
|
|
19
|
-
* before `checkRedirect` and before `cluster.update` validates a single signature, and inbound
|
|
20
|
-
* stream authorization is opt-in (`authorizeInboundStream` is undefined by default), so the map
|
|
21
|
-
* is attacker-authored at that point. One 1 MiB control message (`MAX_CONTROL_MESSAGE_BYTES`)
|
|
22
|
-
* holds on the order of a thousand fabricated `{ id, multiaddrs, publicKey }` entries, each of
|
|
23
|
-
* which would otherwise become a persisted peerStore record. Real cohorts are `clusterSize`
|
|
24
|
-
* peers — single digits — so this is generous margin, not a functional limit.
|
|
25
|
-
*/
|
|
26
|
-
export const MAX_LEARNED_PEERS_PER_RECORD = 64
|
|
27
|
-
|
|
28
|
-
/** The narrow slice of libp2p this module needs — a peer id and (optionally) a peerStore writer. */
|
|
29
|
-
export interface PeerAddressBookHost {
|
|
30
|
-
peerId: PeerId
|
|
31
|
-
peerStore?: {
|
|
32
|
-
merge?: (id: PeerId, data: { multiaddrs: Multiaddr[] }) => Promise<unknown>
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Log sink shaped like both `debug` loggers and libp2p's `Logger`. */
|
|
37
|
-
export type AddressLog = (fmt: string, ...args: unknown[]) => void
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Keep only the entries that parse as multiaddrs, logging (but not throwing on) the rest.
|
|
41
|
-
*
|
|
42
|
-
* The single validator for address strings arriving from the wire or from a connection —
|
|
43
|
-
* `Libp2pKeyPeerNetwork.parseMultiaddrs` delegates here so there is one definition of
|
|
44
|
-
* "an address string we are willing to carry".
|
|
45
|
-
*/
|
|
46
|
-
export function validMultiaddrStrings(addrs: string[], log: AddressLog): string[] {
|
|
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
|
-
|
|
1
|
+
import type { PeerId } from '@libp2p/interface'
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id'
|
|
3
|
+
import { multiaddr, type Component, type Multiaddr } from '@multiformats/multiaddr'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cap on addresses merged per peer from one application-level message.
|
|
7
|
+
*
|
|
8
|
+
* Without a cap, a crafted cluster record or redirect payload could stuff the address
|
|
9
|
+
* book and turn every cohort member into a dial amplifier aimed at an address of the
|
|
10
|
+
* sender's choosing. This bounds the per-peer cost;
|
|
11
|
+
* {@link MAX_LEARNED_PEERS_PER_RECORD} bounds how many peers one record may introduce.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_MERGED_ADDRS_PER_PEER = 8
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cap on how many distinct peers one cluster record may teach us addresses for.
|
|
17
|
+
*
|
|
18
|
+
* A record's peer map is NOT self-limiting: `ClusterService.processOperation` learns from it
|
|
19
|
+
* before `checkRedirect` and before `cluster.update` validates a single signature, and inbound
|
|
20
|
+
* stream authorization is opt-in (`authorizeInboundStream` is undefined by default), so the map
|
|
21
|
+
* is attacker-authored at that point. One 1 MiB control message (`MAX_CONTROL_MESSAGE_BYTES`)
|
|
22
|
+
* holds on the order of a thousand fabricated `{ id, multiaddrs, publicKey }` entries, each of
|
|
23
|
+
* which would otherwise become a persisted peerStore record. Real cohorts are `clusterSize`
|
|
24
|
+
* peers — single digits — so this is generous margin, not a functional limit.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_LEARNED_PEERS_PER_RECORD = 64
|
|
27
|
+
|
|
28
|
+
/** The narrow slice of libp2p this module needs — a peer id and (optionally) a peerStore writer. */
|
|
29
|
+
export interface PeerAddressBookHost {
|
|
30
|
+
peerId: PeerId
|
|
31
|
+
peerStore?: {
|
|
32
|
+
merge?: (id: PeerId, data: { multiaddrs: Multiaddr[] }) => Promise<unknown>
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Log sink shaped like both `debug` loggers and libp2p's `Logger`. */
|
|
37
|
+
export type AddressLog = (fmt: string, ...args: unknown[]) => void
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Keep only the entries that parse as multiaddrs, logging (but not throwing on) the rest.
|
|
41
|
+
*
|
|
42
|
+
* The single validator for address strings arriving from the wire or from a connection —
|
|
43
|
+
* `Libp2pKeyPeerNetwork.parseMultiaddrs` delegates here so there is one definition of
|
|
44
|
+
* "an address string we are willing to carry".
|
|
45
|
+
*/
|
|
46
|
+
export function validMultiaddrStrings(addrs: string[], log: AddressLog): string[] {
|
|
47
|
+
return addrs.filter(a => isCarriableMultiaddrString(a, log))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One address string's verdict, shared by {@link validMultiaddrStrings} and {@link publishableConnectionAddr}. */
|
|
51
|
+
function isCarriableMultiaddrString(addr: string, log: AddressLog): boolean {
|
|
52
|
+
try {
|
|
53
|
+
// An empty string parses as the root multiaddr `/` — syntactically fine, addresses
|
|
54
|
+
// nothing, and encodes to zero bytes. Reject it so a blank entry can't occupy a slot
|
|
55
|
+
// in the address book (or in the per-message cap).
|
|
56
|
+
if (multiaddr(addr).bytes.length === 0) {
|
|
57
|
+
log('WARN: multiaddr addresses nothing %s', addr)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log('WARN: invalid multiaddr %s %o', addr, err)
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The slice of a libp2p `Connection` that decides whether its remote address may be published.
|
|
69
|
+
*
|
|
70
|
+
* `direction` is populated on every connection libp2p creates; it is optional here only because
|
|
71
|
+
* unit stubs build connection literals by hand — and a missing `direction` is deliberately treated
|
|
72
|
+
* as NOT publishable, so a stub cannot silently opt back into the pre-fix behavior.
|
|
73
|
+
*/
|
|
74
|
+
export interface DirectionalConnection {
|
|
75
|
+
direction?: 'inbound' | 'outbound'
|
|
76
|
+
remoteAddr?: { toString?: () => string }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A live connection's remote address, when it is one we may publish to a **third** party —
|
|
81
|
+
* otherwise `undefined`.
|
|
82
|
+
*
|
|
83
|
+
* The companion to {@link validMultiaddrStrings}: that answers "an address string we are willing
|
|
84
|
+
* to carry", this answers "an address we are willing to hand to someone else". They are not the
|
|
85
|
+
* same question, because an inbound connection's `remoteAddr` is not an address at all in the
|
|
86
|
+
* sense a third party needs. For an **outbound** connection it is the address we dialed — a real
|
|
87
|
+
* listen (or circuit) address that anyone can reach the peer on. For an **inbound** one it is the
|
|
88
|
+
* far side's *ephemeral source socket*: the port their operating system picked for this single
|
|
89
|
+
* connection. It is reachable by nobody else, it is indistinguishable from a listen address once
|
|
90
|
+
* it is on the wire, and it takes a slot against {@link MAX_MERGED_ADDRS_PER_PEER} in every peer
|
|
91
|
+
* that merges it. So the cure has to be here, at the producer.
|
|
92
|
+
*
|
|
93
|
+
* An inbound-only peer loses nothing by this: its own advertised addresses reach us through
|
|
94
|
+
* `identify`/`identifyPush` and are published from the peerStore instead.
|
|
95
|
+
*/
|
|
96
|
+
export function publishableConnectionAddr(conn: DirectionalConnection, log: AddressLog): string | undefined {
|
|
97
|
+
if (conn.direction !== 'outbound') return undefined
|
|
98
|
+
const addr = conn.remoteAddr?.toString?.()
|
|
99
|
+
if (addr === undefined) return undefined
|
|
100
|
+
return isCarriableMultiaddrString(addr, log) ? addr : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* How useful the addresses we hold for a peer are **to this node's own dialer**.
|
|
105
|
+
*
|
|
106
|
+
* - `none` — we hold no address at all. Nobody has told us how to reach the peer.
|
|
107
|
+
* - `self-relay-only` — we hold addresses, but every one of them reaches the peer by relaying
|
|
108
|
+
* through *us*. Useful to everyone except us: to use one we would have to relay to the peer
|
|
109
|
+
* through ourselves.
|
|
110
|
+
* - `dialable` — at least one address does not route through us, so a dial can be attempted.
|
|
111
|
+
*/
|
|
112
|
+
export type SelfDialability = 'none' | 'self-relay-only' | 'dialable'
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Does `addr` reach its target by relaying through `relayPeerId`?
|
|
116
|
+
*
|
|
117
|
+
* A circuit multiaddr names its relay in the `p2p` component immediately BEFORE the
|
|
118
|
+
* `p2p-circuit` marker — `/<transport>/p2p/<relay>/p2p-circuit[/p2p/<target>]`. So the question is
|
|
119
|
+
* answered by walking the address's components, not by testing the string for `/p2p/<id>/p2p-circuit`:
|
|
120
|
+
* the peer id after the marker (appended by libp2p's dial queue), a bare `/p2p-circuit` with no
|
|
121
|
+
* relay named, and multi-hop addresses with two circuit markers all read differently as text but
|
|
122
|
+
* classify correctly as components. True if ANY hop relays through `relayPeerId` — a chain that
|
|
123
|
+
* passes through us at any point is one we cannot open ourselves.
|
|
124
|
+
*
|
|
125
|
+
* Called with our own peer id, this is the "can WE dial this?" question. It is deliberately NOT
|
|
126
|
+
* the same question as {@link publishableConnectionAddr}'s: a self-relay address is perfectly
|
|
127
|
+
* publishable — a cohort sibling reaching a peer through our relay is the working path — and is
|
|
128
|
+
* simply unusable by the one node the circuit terminates on.
|
|
129
|
+
*/
|
|
130
|
+
export function routesThroughRelay(addr: string, relayPeerId: string, log: AddressLog): boolean {
|
|
131
|
+
let components: Component[]
|
|
132
|
+
try {
|
|
133
|
+
components = multiaddr(addr).getComponents()
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Fail open. An address we cannot parse is not evidence of a self-relay loop, and the
|
|
136
|
+
// caller's fallback — dial it and let libp2p reject it — is the pre-existing behavior.
|
|
137
|
+
log('WARN: invalid multiaddr %s %o', addr, err)
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return components.some((component, i) =>
|
|
141
|
+
component.name === 'p2p-circuit' && isRelayComponent(components[i - 1], relayPeerId))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* True when `component` is the `p2p` hop naming `relayPeerId` (absent/other component → false).
|
|
146
|
+
*
|
|
147
|
+
* `relayPeerId` is always a `PeerId.toString()`, i.e. base58btc. A multiaddr's `p2p` value usually
|
|
148
|
+
* is too — but it may equally be written as a CIDv1 libp2p-key string, and `@multiformats/multiaddr`
|
|
149
|
+
* keeps whichever form it was given rather than normalizing. String equality alone would therefore
|
|
150
|
+
* miss a CIDv1-form self-relay address arriving from the wire (`mergePeerAddresses` accepts any
|
|
151
|
+
* parseable multiaddr). The canonical compare runs only when the cheap one fails, and only for the
|
|
152
|
+
* single component sitting in front of a circuit marker, so the parse is bounded to circuit
|
|
153
|
+
* addresses rather than paid per address.
|
|
154
|
+
*/
|
|
155
|
+
function isRelayComponent(component: Component | undefined, relayPeerId: string): boolean {
|
|
156
|
+
if (component?.name !== 'p2p' || component.value === undefined) return false
|
|
157
|
+
if (component.value === relayPeerId) return true
|
|
158
|
+
try {
|
|
159
|
+
return peerIdFromString(component.value).toString() === relayPeerId
|
|
160
|
+
} catch {
|
|
161
|
+
// multiaddr accepted the component but we cannot read it back as a peer id; we simply
|
|
162
|
+
// cannot claim it is ours, and the address stays dialable-as-far-as-we-know.
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Classify what the addresses we hold for one peer are worth to our own dialer.
|
|
169
|
+
*
|
|
170
|
+
* `self-relay-only` is the state a relay lands in for its own reservation holders: the address
|
|
171
|
+
* such a client advertises — and therefore the one we learn through `identifyPush` and store — is
|
|
172
|
+
* `/<our transport addr>/p2p/<our peer id>/p2p-circuit`. Dialing it is guaranteed to fail, and the
|
|
173
|
+
* failure is indistinguishable from `none` in libp2p's error text, so the two are separated here
|
|
174
|
+
* instead. Nothing can repair it from our side: once the client's connection drops only the client
|
|
175
|
+
* can re-initiate, so the useful response is to fail fast and let the caller move on.
|
|
176
|
+
*/
|
|
177
|
+
export function classifySelfDialability(addrs: string[], selfPeerId: string, log: AddressLog): SelfDialability {
|
|
178
|
+
if (addrs.length === 0) return 'none'
|
|
179
|
+
return addrs.some(addr => !routesThroughRelay(addr, selfPeerId, log)) ? 'dialable' : 'self-relay-only'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Write dialable addresses for `peerId` into the libp2p address book, from addresses
|
|
184
|
+
* carried by an application-level message.
|
|
185
|
+
*
|
|
186
|
+
* Trust boundary: a merged multiaddr only makes a dial *attempt* possible. The dialed
|
|
187
|
+
* peer still authenticates by peer id at the noise handshake, so an address taken from
|
|
188
|
+
* a record we have not otherwise verified can waste a dial but can never impersonate.
|
|
189
|
+
* That is precisely why it is safe to consume addresses from an unverified message —
|
|
190
|
+
* and why the cost, not the authenticity, is what needs bounding (see
|
|
191
|
+
* {@link MAX_MERGED_ADDRS_PER_PEER}).
|
|
192
|
+
*/
|
|
193
|
+
export function mergePeerAddresses(
|
|
194
|
+
host: PeerAddressBookHost,
|
|
195
|
+
peerId: PeerId,
|
|
196
|
+
addrs: string[],
|
|
197
|
+
log: AddressLog
|
|
198
|
+
): void {
|
|
199
|
+
// A self entry is meaningless to our own dialer and, for a relay-only self, self-referential.
|
|
200
|
+
if (peerId.toString() === host.peerId.toString()) return
|
|
201
|
+
if (addrs.length === 0) return
|
|
202
|
+
|
|
203
|
+
const merge = host.peerStore?.merge
|
|
204
|
+
if (typeof merge !== 'function') return
|
|
205
|
+
|
|
206
|
+
const valid = validMultiaddrStrings(addrs, log)
|
|
207
|
+
if (valid.length === 0) return
|
|
208
|
+
if (valid.length > MAX_MERGED_ADDRS_PER_PEER) {
|
|
209
|
+
log('peer-address-book:capped peer=%s offered=%d kept=%d',
|
|
210
|
+
peerId.toString().substring(0, 12), valid.length, MAX_MERGED_ADDRS_PER_PEER)
|
|
211
|
+
}
|
|
212
|
+
const multiaddrs = valid.slice(0, MAX_MERGED_ADDRS_PER_PEER).map(a => multiaddr(a))
|
|
213
|
+
|
|
214
|
+
log('peer-address-book:merge peer=%s addrs=%d', peerId.toString().substring(0, 12), multiaddrs.length)
|
|
215
|
+
// `merge` is async and nothing downstream awaits the address book — the very next dial
|
|
216
|
+
// either sees the entry or falls back to the same failure it had before. Log a rejection
|
|
217
|
+
// rather than swallowing it: a persistently failing peerStore is exactly the condition
|
|
218
|
+
// that would make this whole mechanism silently inert.
|
|
219
|
+
void Promise.resolve(merge.call(host.peerStore, peerId, { multiaddrs }))
|
|
220
|
+
.catch((err: unknown) => log('WARN: peerStore.merge failed peer=%s %o', peerId.toString().substring(0, 12), err))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The peer map a `ClusterRecord` carries, as it arrives off the wire (nothing about it is trusted). */
|
|
224
|
+
export type RecordPeerMap = Record<string, { multiaddrs?: string[] } | undefined>
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Offer the addresses a cluster record carries for its cohort to an address-book `sink`, one
|
|
228
|
+
* peer at a time.
|
|
229
|
+
*
|
|
230
|
+
* The one traversal shared by both record ingress points — `ClusterService` (inbound, from the
|
|
231
|
+
* coordinator) and `ClusterClient` (outbound, from a member's reply) — so the entries a record is
|
|
232
|
+
* allowed to introduce are bounded in one place rather than two. Entries with no addresses, with
|
|
233
|
+
* an id equal to `skipId`, or with an unparseable id are dropped; everything past
|
|
234
|
+
* {@link MAX_LEARNED_PEERS_PER_RECORD} candidates is dropped with a log line. The per-address
|
|
235
|
+
* validation, the per-peer cap, and the trust boundary live behind `sink`
|
|
236
|
+
* (see {@link mergePeerAddresses}).
|
|
237
|
+
*/
|
|
238
|
+
export function mergeRecordPeerAddresses(
|
|
239
|
+
peers: RecordPeerMap | undefined,
|
|
240
|
+
sink: (peerId: PeerId, addrs: string[]) => void,
|
|
241
|
+
log: AddressLog,
|
|
242
|
+
skipId?: string
|
|
243
|
+
): void {
|
|
244
|
+
let offered = 0
|
|
245
|
+
for (const [idStr, peer] of Object.entries(peers ?? {})) {
|
|
246
|
+
const addrs = peer?.multiaddrs ?? []
|
|
247
|
+
if (addrs.length === 0 || idStr === skipId) continue
|
|
248
|
+
if (offered >= MAX_LEARNED_PEERS_PER_RECORD) {
|
|
249
|
+
// Count candidates, not successes, so a record full of unparseable ids cannot spend
|
|
250
|
+
// unbounded parse attempts and log lines either.
|
|
251
|
+
log('peer-address-book:record-capped kept=%d', MAX_LEARNED_PEERS_PER_RECORD)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
offered += 1
|
|
255
|
+
let pid: PeerId
|
|
256
|
+
try {
|
|
257
|
+
pid = peerIdFromString(idStr)
|
|
258
|
+
} catch (err) {
|
|
259
|
+
// An id we cannot parse is not dialable by any route; the consensus path surfaces the
|
|
260
|
+
// resulting membership failure on its own.
|
|
261
|
+
log('WARN: record carried an unparseable peer id %s %o', idStr, err)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
sink(pid, addrs)
|
|
265
|
+
}
|
|
266
|
+
}
|