p2p-fret 1.0.0-beta.4 → 1.0.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/dist/src/index.d.ts +32 -3
- package/dist/src/rpc/neighbors.d.ts +3 -1
- package/dist/src/rpc/neighbors.js +3 -1
- package/dist/src/rpc/request.js +1 -1
- package/dist/src/rpc/validate.d.ts +35 -16
- package/dist/src/rpc/validate.js +84 -17
- package/dist/src/service/address-records.d.ts +136 -0
- package/dist/src/service/address-records.js +262 -0
- package/dist/src/service/fret-service.d.ts +106 -11
- package/dist/src/service/fret-service.js +324 -26
- package/dist/src/service/libp2p-fret-service.d.ts +8 -1
- package/dist/src/service/libp2p-fret-service.js +3 -1
- package/dist/src/service/peer-discovery.js +5 -5
- package/dist/src/store/digitree-store.d.ts +41 -4
- package/dist/src/store/digitree-store.js +33 -6
- package/dist/src/store/relevance.d.ts +8 -0
- package/dist/src/store/relevance.js +8 -0
- package/package.json +3 -1
- package/src/index.ts +33 -3
- package/src/ring/ring-walk.ts +110 -110
- package/src/rpc/leave.ts +77 -77
- package/src/rpc/maybe-act.ts +85 -85
- package/src/rpc/neighbors.ts +156 -154
- package/src/rpc/outcome.ts +66 -66
- package/src/rpc/ping.ts +100 -100
- package/src/rpc/request.ts +310 -310
- package/src/rpc/validate.ts +79 -17
- package/src/selector/next-hop.ts +325 -325
- package/src/service/address-records.ts +275 -0
- package/src/service/dedup-cache.ts +53 -53
- package/src/service/fret-service.ts +312 -27
- package/src/service/libp2p-fret-service.ts +239 -231
- package/src/service/live-member.ts +29 -29
- package/src/service/peer-discovery.ts +205 -205
- package/src/service/probe-backoff.ts +181 -181
- package/src/service/size-observer.ts +227 -227
- package/src/store/digitree-store.ts +699 -642
- package/src/store/relevance.ts +201 -193
- package/src/utils/deadline.ts +85 -85
- package/src/utils/expiring-map.ts +164 -164
- package/src/utils/pool.ts +79 -79
- package/src/utils/token-bucket.ts +70 -70
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Libp2p } from 'libp2p';
|
|
2
|
-
import type {
|
|
2
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
3
|
+
import type { SerializedAddressRecord, SerializedPeerEntry, SerializedTable } from './store/digitree-store.js';
|
|
3
4
|
import { FretService as FretServiceClass } from './service/fret-service.js';
|
|
4
5
|
export type FretMode = 'active' | 'passive';
|
|
5
6
|
export interface FretConfig {
|
|
@@ -17,6 +18,28 @@ export interface FretConfig {
|
|
|
17
18
|
* spacing window count once, so the run is genuinely spread over time.
|
|
18
19
|
*/
|
|
19
20
|
deadAfterFailures?: number;
|
|
21
|
+
/**
|
|
22
|
+
* This node's libp2p private key — the key `node.peerId` was derived from.
|
|
23
|
+
*
|
|
24
|
+
* Used to seal this node's own signed address record (a libp2p `PeerRecord` envelope) for the
|
|
25
|
+
* `hints` field of its outgoing neighbour snapshots, so peers that only ever hear of this node
|
|
26
|
+
* through FRET can still dial it. libp2p hands the key to services as the `privateKey`
|
|
27
|
+
* component, so `Libp2pFretService` fills this in automatically; a caller constructing the core
|
|
28
|
+
* service directly passes it here. The constructor throws if the key does not derive
|
|
29
|
+
* `node.peerId`. Absent → this node advertises no record for itself (logged once at `start()`);
|
|
30
|
+
* forwarding other peers' records and consuming received ones are unaffected.
|
|
31
|
+
*/
|
|
32
|
+
privateKey?: PrivateKey;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One signed address record riding on a neighbour snapshot: `record` is the base64url encoding
|
|
36
|
+
* of a marshaled libp2p `RecordEnvelope` over a `PeerRecord` whose signer and payload peer id are
|
|
37
|
+
* both `id`. The receiver verifies all of that before the record touches its peerStore — see
|
|
38
|
+
* `FretService.ingestAddressHints`.
|
|
39
|
+
*/
|
|
40
|
+
export interface AddressHintV1 {
|
|
41
|
+
id: string;
|
|
42
|
+
record: string;
|
|
20
43
|
}
|
|
21
44
|
export interface NeighborSnapshotV1 {
|
|
22
45
|
v: 1;
|
|
@@ -33,6 +56,12 @@ export interface NeighborSnapshotV1 {
|
|
|
33
56
|
confidence?: number;
|
|
34
57
|
sig: string;
|
|
35
58
|
metadata?: Record<string, unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* Signed address records for peers this snapshot names (`from`, the id lists, the sample).
|
|
61
|
+
* Optional and unversioned: a reader that predates it ignores the field. An array rather than
|
|
62
|
+
* an id-keyed object so it truncates like the other lists and has no `__proto__`-key hazard.
|
|
63
|
+
*/
|
|
64
|
+
hints?: AddressHintV1[];
|
|
36
65
|
}
|
|
37
66
|
export interface RouteAndMaybeActV1 {
|
|
38
67
|
v: 1;
|
|
@@ -121,7 +150,7 @@ export interface FretService {
|
|
|
121
150
|
exportTable(): SerializedTable;
|
|
122
151
|
importTable(table: SerializedTable): Promise<number>;
|
|
123
152
|
}
|
|
124
|
-
export type { SerializedPeerEntry, SerializedTable };
|
|
153
|
+
export type { SerializedAddressRecord, SerializedPeerEntry, SerializedTable };
|
|
125
154
|
export { FretServiceClass as FretServiceImpl };
|
|
126
155
|
export { FretPeerDiscovery, type DiscoverySnapshotSource, type FretPeerDiscoveryInput, type FretPeerDiscoveryConfig } from './service/peer-discovery.js';
|
|
127
156
|
export { Libp2pFretService, fretService } from './service/libp2p-fret-service.js';
|
|
@@ -129,7 +158,7 @@ export { hashKey, hashPeerId } from './ring/hash.js';
|
|
|
129
158
|
export type { RingCoord } from './ring/hash.js';
|
|
130
159
|
export { clockwiseDistance, minDistance, lexLess } from './ring/distance.js';
|
|
131
160
|
export { DigitreeStore } from './store/digitree-store.js';
|
|
132
|
-
export type { PeerEntry, PeerPatch, PeerState, MembershipState, RingCursor, RingWalkPage } from './store/digitree-store.js';
|
|
161
|
+
export type { PeerEntry, PeerAddressRecord, PeerPatch, PeerState, MembershipState, RingCursor, RingWalkPage } from './store/digitree-store.js';
|
|
133
162
|
export { estimateSizeAndConfidence } from './estimate/size-estimator.js';
|
|
134
163
|
export type { SizeEstimate, SizeEstimateOptions } from './estimate/size-estimator.js';
|
|
135
164
|
export { assembleCohort } from './service/cohort.js';
|
|
@@ -35,7 +35,9 @@ export declare function fetchNeighbors(node: Libp2p, peerIdOrStr: string, protoc
|
|
|
35
35
|
* that deliberately target *non-connected* peers — the "tell peers we just learned about"
|
|
36
36
|
* paths, whose whole point is reaching someone we are not talking to yet — pass `dial: true`.
|
|
37
37
|
* Those callers own the reachability check: dialing a peer libp2p holds no address for can
|
|
38
|
-
* only fail
|
|
38
|
+
* only fail. The dial itself is by bare peer id; the addresses it resolves to are whatever the
|
|
39
|
+
* peerStore holds, which since address hints includes the signed records FRET verified out of
|
|
40
|
+
* earlier snapshots (`FretService.ingestAddressHints`).
|
|
39
41
|
*/
|
|
40
42
|
export declare function announceNeighbors(node: Libp2p, peerIdOrStr: string, snapshot: NeighborSnapshotV1, protocol?: string, opts?: {
|
|
41
43
|
dial?: boolean;
|
|
@@ -106,7 +106,9 @@ export async function fetchNeighbors(node, peerIdOrStr, protocol = PROTOCOL_NEIG
|
|
|
106
106
|
* that deliberately target *non-connected* peers — the "tell peers we just learned about"
|
|
107
107
|
* paths, whose whole point is reaching someone we are not talking to yet — pass `dial: true`.
|
|
108
108
|
* Those callers own the reachability check: dialing a peer libp2p holds no address for can
|
|
109
|
-
* only fail
|
|
109
|
+
* only fail. The dial itself is by bare peer id; the addresses it resolves to are whatever the
|
|
110
|
+
* peerStore holds, which since address hints includes the signed records FRET verified out of
|
|
111
|
+
* earlier snapshots (`FretService.ingestAddressHints`).
|
|
110
112
|
*/
|
|
111
113
|
export async function announceNeighbors(node, peerIdOrStr, snapshot, protocol = PROTOCOL_NEIGHBORS_ANNOUNCE, opts = {}) {
|
|
112
114
|
// Write-only: `ok` proves the body reached the transport, nothing more. No swallow — the
|
package/dist/src/rpc/request.js
CHANGED
|
@@ -4,7 +4,7 @@ import { abortReasonError, deadline } from '../utils/deadline.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Default declared-length cap for a reply when the caller passes none: 8 KiB. It tracks no other
|
|
6
6
|
* cap in the system — every FRET sender passes its own explicitly (`sendPing` 1024, the neighbors
|
|
7
|
-
* senders `MAX_NEIGHBORS_BYTES` (
|
|
7
|
+
* senders `MAX_NEIGHBORS_BYTES` (64 KiB), maybeAct 144 KiB), so this value is only ever reached by a
|
|
8
8
|
* direct consumer of `rpcRequest` who forgot one. 8 KiB is chosen as a conservative floor for a
|
|
9
9
|
* reply whose shape we do not know: large enough that a small JSON reply is never truncated,
|
|
10
10
|
* small enough that a forgotten cap still bounds the read.
|
|
@@ -41,42 +41,61 @@ export declare const MAYBE_ACT_OVERHEAD_BYTES: number;
|
|
|
41
41
|
* Per-message byte cap for both neighbors protocols (request/reply and announce). **One number
|
|
42
42
|
* for both profiles**, because this is an *acceptance* limit: it bounds the largest message a
|
|
43
43
|
* *peer* may send us, and Edge and Core peers talk to each other, so it has to cover the largest
|
|
44
|
-
* snapshot any profile can legitimately emit. Sizing it per-profile — as it was, Core 16 KiB /
|
|
44
|
+
* snapshot any profile can legitimately emit. Sizing it per-profile — as it once was, Core 16 KiB /
|
|
45
45
|
* Edge 8 KiB — made a legal Core snapshot unreadable by every Edge peer.
|
|
46
46
|
*
|
|
47
47
|
* Derived, not picked. The Core worst legal emission, measured by encoding the shape
|
|
48
48
|
* `FretService.snapshot` produces (53-char peer ids, a 32-byte base64url coordinate per sample
|
|
49
49
|
* entry, `metadata` grown until `JSON.stringify(metadata)` hits
|
|
50
|
-
* {@link MAX_SNAPSHOT_METADATA_BYTES_CORE} exactly,
|
|
51
|
-
* `
|
|
50
|
+
* {@link MAX_SNAPSHOT_METADATA_BYTES_CORE} exactly, 256 bytes reserved for the still-empty `sig`
|
|
51
|
+
* field, and a `hints` array packed to {@link MAX_SNAPSHOT_HINT_BYTES_CORE}):
|
|
52
52
|
*
|
|
53
53
|
* 16 successors + 16 predecessors + 8 sample entries (the *merge* caps, which are wider than
|
|
54
54
|
* today's emission caps of 12/12/8) .................................... 3,371 bytes
|
|
55
55
|
* + 8 KiB metadata (including the `,"metadata":` key) .................. 8,204 bytes
|
|
56
|
-
* = 11,575 bytes
|
|
56
|
+
* = 11,575 bytes of fixed fields
|
|
57
|
+
* + 48 KiB of hints (the whole array, plus its `,"hints":[]` framing) .... 49,163 bytes
|
|
58
|
+
* = 60,738 bytes
|
|
57
59
|
*
|
|
58
|
-
*
|
|
59
|
-
* the number, so landing message signatures does not disturb it.
|
|
60
|
+
* 64 KiB leaves ~4.7 KiB of headroom over that. Sereus used the same 64 KiB for the same payload.
|
|
60
61
|
*
|
|
61
62
|
* The invariant this constant carries, pinned by `test/rpc.codec-properties.spec.ts`: **the
|
|
62
|
-
* largest legal emission of *any* profile
|
|
63
|
-
* breaks that and fails there.
|
|
63
|
+
* largest legal emission of *any* profile — fixed fields, metadata allowance and hint budget
|
|
64
|
+
* together — encodes under this cap.** Re-splitting it per profile breaks that and fails there.
|
|
64
65
|
*/
|
|
65
66
|
export declare const MAX_NEIGHBORS_BYTES: number;
|
|
66
67
|
/** Cap on the encoded size of the caller-supplied `metadata` a Core node attaches to its
|
|
67
68
|
* outgoing snapshot. This is an **emission budget** — the Core profile's own choice of how much
|
|
68
69
|
* application metadata it will carry — not an acceptance limit; acceptance is
|
|
69
|
-
* {@link MAX_NEIGHBORS_BYTES}, one network-wide number. The invariant the
|
|
70
|
-
* is `largest emission of any profile <= MAX_NEIGHBORS_BYTES`: at the merge caps the Core
|
|
71
|
-
* fields cost 3,371 bytes, so this allowance plus them lands at 11,575,
|
|
72
|
-
*
|
|
73
|
-
* `FretService.snapshot`. */
|
|
70
|
+
* {@link MAX_NEIGHBORS_BYTES}, one network-wide number. The invariant the emission budgets must
|
|
71
|
+
* keep is `largest emission of any profile <= MAX_NEIGHBORS_BYTES`: at the merge caps the Core
|
|
72
|
+
* fixed fields cost 3,371 bytes, so this allowance plus them lands at 11,575, and the hint budget
|
|
73
|
+
* ({@link MAX_SNAPSHOT_HINT_BYTES_CORE}) on top of that at 60,738 — under. Over-cap metadata is
|
|
74
|
+
* omitted from the snapshot rather than truncated — see `FretService.snapshot`. */
|
|
74
75
|
export declare const MAX_SNAPSHOT_METADATA_BYTES_CORE: number;
|
|
75
76
|
/** Same, Edge profile: an emission budget, half of Core's because an edge node carries less.
|
|
76
|
-
* Its worst legal emission is 6,301 bytes
|
|
77
|
-
*
|
|
78
|
-
* acceptance cap; there is no such thing. */
|
|
77
|
+
* Its worst legal emission is 6,301 bytes of fixed fields plus {@link MAX_SNAPSHOT_HINT_BYTES_EDGE}
|
|
78
|
+
* of hints, also under {@link MAX_NEIGHBORS_BYTES} — which is the only bound any budget has to
|
|
79
|
+
* satisfy. Deliberately *not* sized against a per-profile acceptance cap; there is no such thing. */
|
|
79
80
|
export declare const MAX_SNAPSHOT_METADATA_BYTES_EDGE: number;
|
|
81
|
+
/**
|
|
82
|
+
* Cap on one base64url-encoded address record in a snapshot's `hints`, in characters (one byte
|
|
83
|
+
* each — base64url is ASCII). An Ed25519 envelope over a handful of addresses encodes to roughly
|
|
84
|
+
* 300–700 characters; 2048 leaves room for an RSA key and the eight addresses a self record may
|
|
85
|
+
* carry (`MAX_SELF_RECORD_ADDRS` in `service/address-records.ts`). Applied on both sides: the parser drops a longer record, and
|
|
86
|
+
* `SelfAddressRecord` omits its own record rather than emit one the receiver would drop.
|
|
87
|
+
*/
|
|
88
|
+
export declare const MAX_ADDRESS_RECORD_CHARS = 2048;
|
|
89
|
+
/**
|
|
90
|
+
* Emission budget for the whole `hints` array of a Core snapshot — the sum over hints of
|
|
91
|
+
* `JSON.stringify(hint).length + 1` (the `+ 1` is the separating comma), which is what
|
|
92
|
+
* `FretService.snapshot` counts. Profile-split like the metadata allowance, and for the same
|
|
93
|
+
* reason: it is this node's own choice of how much it forwards, not a limit on what it accepts.
|
|
94
|
+
* Worst case: 11,575 (fixed fields) + 49,152 + the field's own framing < {@link MAX_NEIGHBORS_BYTES}.
|
|
95
|
+
*/
|
|
96
|
+
export declare const MAX_SNAPSHOT_HINT_BYTES_CORE: number;
|
|
97
|
+
/** Same, Edge profile: an edge node forwards less. 6,301 + 16,384 + framing is far under the cap. */
|
|
98
|
+
export declare const MAX_SNAPSHOT_HINT_BYTES_EDGE: number;
|
|
80
99
|
/**
|
|
81
100
|
* Structural validity of an inbound `RouteAndMaybeAct` — everything downstream code touches
|
|
82
101
|
* without checking, and nothing more. The caller runs it immediately after taking the rate-limit
|
package/dist/src/rpc/validate.js
CHANGED
|
@@ -71,42 +71,61 @@ export const MAYBE_ACT_OVERHEAD_BYTES = 16 * 1024;
|
|
|
71
71
|
* Per-message byte cap for both neighbors protocols (request/reply and announce). **One number
|
|
72
72
|
* for both profiles**, because this is an *acceptance* limit: it bounds the largest message a
|
|
73
73
|
* *peer* may send us, and Edge and Core peers talk to each other, so it has to cover the largest
|
|
74
|
-
* snapshot any profile can legitimately emit. Sizing it per-profile — as it was, Core 16 KiB /
|
|
74
|
+
* snapshot any profile can legitimately emit. Sizing it per-profile — as it once was, Core 16 KiB /
|
|
75
75
|
* Edge 8 KiB — made a legal Core snapshot unreadable by every Edge peer.
|
|
76
76
|
*
|
|
77
77
|
* Derived, not picked. The Core worst legal emission, measured by encoding the shape
|
|
78
78
|
* `FretService.snapshot` produces (53-char peer ids, a 32-byte base64url coordinate per sample
|
|
79
79
|
* entry, `metadata` grown until `JSON.stringify(metadata)` hits
|
|
80
|
-
* {@link MAX_SNAPSHOT_METADATA_BYTES_CORE} exactly,
|
|
81
|
-
* `
|
|
80
|
+
* {@link MAX_SNAPSHOT_METADATA_BYTES_CORE} exactly, 256 bytes reserved for the still-empty `sig`
|
|
81
|
+
* field, and a `hints` array packed to {@link MAX_SNAPSHOT_HINT_BYTES_CORE}):
|
|
82
82
|
*
|
|
83
83
|
* 16 successors + 16 predecessors + 8 sample entries (the *merge* caps, which are wider than
|
|
84
84
|
* today's emission caps of 12/12/8) .................................... 3,371 bytes
|
|
85
85
|
* + 8 KiB metadata (including the `,"metadata":` key) .................. 8,204 bytes
|
|
86
|
-
* = 11,575 bytes
|
|
86
|
+
* = 11,575 bytes of fixed fields
|
|
87
|
+
* + 48 KiB of hints (the whole array, plus its `,"hints":[]` framing) .... 49,163 bytes
|
|
88
|
+
* = 60,738 bytes
|
|
87
89
|
*
|
|
88
|
-
*
|
|
89
|
-
* the number, so landing message signatures does not disturb it.
|
|
90
|
+
* 64 KiB leaves ~4.7 KiB of headroom over that. Sereus used the same 64 KiB for the same payload.
|
|
90
91
|
*
|
|
91
92
|
* The invariant this constant carries, pinned by `test/rpc.codec-properties.spec.ts`: **the
|
|
92
|
-
* largest legal emission of *any* profile
|
|
93
|
-
* breaks that and fails there.
|
|
93
|
+
* largest legal emission of *any* profile — fixed fields, metadata allowance and hint budget
|
|
94
|
+
* together — encodes under this cap.** Re-splitting it per profile breaks that and fails there.
|
|
94
95
|
*/
|
|
95
|
-
export const MAX_NEIGHBORS_BYTES =
|
|
96
|
+
export const MAX_NEIGHBORS_BYTES = 64 * 1024;
|
|
96
97
|
/** Cap on the encoded size of the caller-supplied `metadata` a Core node attaches to its
|
|
97
98
|
* outgoing snapshot. This is an **emission budget** — the Core profile's own choice of how much
|
|
98
99
|
* application metadata it will carry — not an acceptance limit; acceptance is
|
|
99
|
-
* {@link MAX_NEIGHBORS_BYTES}, one network-wide number. The invariant the
|
|
100
|
-
* is `largest emission of any profile <= MAX_NEIGHBORS_BYTES`: at the merge caps the Core
|
|
101
|
-
* fields cost 3,371 bytes, so this allowance plus them lands at 11,575,
|
|
102
|
-
*
|
|
103
|
-
* `FretService.snapshot`. */
|
|
100
|
+
* {@link MAX_NEIGHBORS_BYTES}, one network-wide number. The invariant the emission budgets must
|
|
101
|
+
* keep is `largest emission of any profile <= MAX_NEIGHBORS_BYTES`: at the merge caps the Core
|
|
102
|
+
* fixed fields cost 3,371 bytes, so this allowance plus them lands at 11,575, and the hint budget
|
|
103
|
+
* ({@link MAX_SNAPSHOT_HINT_BYTES_CORE}) on top of that at 60,738 — under. Over-cap metadata is
|
|
104
|
+
* omitted from the snapshot rather than truncated — see `FretService.snapshot`. */
|
|
104
105
|
export const MAX_SNAPSHOT_METADATA_BYTES_CORE = 8 * 1024;
|
|
105
106
|
/** Same, Edge profile: an emission budget, half of Core's because an edge node carries less.
|
|
106
|
-
* Its worst legal emission is 6,301 bytes
|
|
107
|
-
*
|
|
108
|
-
* acceptance cap; there is no such thing. */
|
|
107
|
+
* Its worst legal emission is 6,301 bytes of fixed fields plus {@link MAX_SNAPSHOT_HINT_BYTES_EDGE}
|
|
108
|
+
* of hints, also under {@link MAX_NEIGHBORS_BYTES} — which is the only bound any budget has to
|
|
109
|
+
* satisfy. Deliberately *not* sized against a per-profile acceptance cap; there is no such thing. */
|
|
109
110
|
export const MAX_SNAPSHOT_METADATA_BYTES_EDGE = 4 * 1024;
|
|
111
|
+
/**
|
|
112
|
+
* Cap on one base64url-encoded address record in a snapshot's `hints`, in characters (one byte
|
|
113
|
+
* each — base64url is ASCII). An Ed25519 envelope over a handful of addresses encodes to roughly
|
|
114
|
+
* 300–700 characters; 2048 leaves room for an RSA key and the eight addresses a self record may
|
|
115
|
+
* carry (`MAX_SELF_RECORD_ADDRS` in `service/address-records.ts`). Applied on both sides: the parser drops a longer record, and
|
|
116
|
+
* `SelfAddressRecord` omits its own record rather than emit one the receiver would drop.
|
|
117
|
+
*/
|
|
118
|
+
export const MAX_ADDRESS_RECORD_CHARS = 2048;
|
|
119
|
+
/**
|
|
120
|
+
* Emission budget for the whole `hints` array of a Core snapshot — the sum over hints of
|
|
121
|
+
* `JSON.stringify(hint).length + 1` (the `+ 1` is the separating comma), which is what
|
|
122
|
+
* `FretService.snapshot` counts. Profile-split like the metadata allowance, and for the same
|
|
123
|
+
* reason: it is this node's own choice of how much it forwards, not a limit on what it accepts.
|
|
124
|
+
* Worst case: 11,575 (fixed fields) + 49,152 + the field's own framing < {@link MAX_NEIGHBORS_BYTES}.
|
|
125
|
+
*/
|
|
126
|
+
export const MAX_SNAPSHOT_HINT_BYTES_CORE = 48 * 1024;
|
|
127
|
+
/** Same, Edge profile: an edge node forwards less. 6,301 + 16,384 + framing is far under the cap. */
|
|
128
|
+
export const MAX_SNAPSHOT_HINT_BYTES_EDGE = 16 * 1024;
|
|
110
129
|
/**
|
|
111
130
|
* Structural validity of an inbound `RouteAndMaybeAct` — everything downstream code touches
|
|
112
131
|
* without checking, and nothing more. The caller runs it immediately after taking the rate-limit
|
|
@@ -247,10 +266,58 @@ export function makeSnapshotParser(caps) {
|
|
|
247
266
|
delete out.metadata;
|
|
248
267
|
else
|
|
249
268
|
out.metadata = msg.metadata;
|
|
269
|
+
// Hints are tied to the ids this message names *after* truncation, so the merge caps above
|
|
270
|
+
// bound the receiver's verify work as well as its hash-and-upsert work: a message cannot
|
|
271
|
+
// carry more records than it carries ids, whatever its `hints` array holds.
|
|
272
|
+
const named = new Set([msg.from, ...out.successors, ...out.predecessors, ...out.sample.map((s) => s.id)]);
|
|
273
|
+
const hints = parseHints(msg.hints, named, msg.from);
|
|
274
|
+
if (hints.length === 0)
|
|
275
|
+
delete out.hints;
|
|
276
|
+
else
|
|
277
|
+
out.hints = hints;
|
|
250
278
|
// `sig` is deliberately unchecked: message signing is unimplemented, so nothing reads it.
|
|
251
279
|
return out;
|
|
252
280
|
};
|
|
253
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* The address hints, vetted per entry against the snapshot's named set — see
|
|
284
|
+
* {@link makeSnapshotParser}. An entry survives when it is an object whose `id` is a named id not
|
|
285
|
+
* already hinted and whose `record` is a base64url string of at most {@link MAX_ADDRESS_RECORD_CHARS}
|
|
286
|
+
* characters. Structural only: the decode proves the string is base64url, nothing about what it
|
|
287
|
+
* encodes — envelope structure and the signature are checked at ingestion, where the store entry
|
|
288
|
+
* and the peerStore are. Same skip-and-log-per-entry rule as the sample; never throws.
|
|
289
|
+
*
|
|
290
|
+
* The count is bounded by the named set's size through the `seen` set, and the loop stops once
|
|
291
|
+
* every named id has a hint, so a long junk array costs a per-entry type check and nothing more.
|
|
292
|
+
*/
|
|
293
|
+
function parseHints(value, named, from) {
|
|
294
|
+
if (!Array.isArray(value))
|
|
295
|
+
return [];
|
|
296
|
+
const out = [];
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
for (const entry of value) {
|
|
299
|
+
if (seen.size >= named.size)
|
|
300
|
+
break;
|
|
301
|
+
if (!isPlainObject(entry))
|
|
302
|
+
continue;
|
|
303
|
+
if (typeof entry.id !== 'string' || !named.has(entry.id) || seen.has(entry.id))
|
|
304
|
+
continue;
|
|
305
|
+
if (typeof entry.record !== 'string' || entry.record.length > MAX_ADDRESS_RECORD_CHARS)
|
|
306
|
+
continue;
|
|
307
|
+
try {
|
|
308
|
+
u8FromString(entry.record, 'base64url');
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
// NOTE: one line per dropped entry, bounded by the named set — the same terms as the
|
|
312
|
+
// sample's per-entry line above.
|
|
313
|
+
log.error('snapshot from %s: dropping address hint for %s with undecodable record - %e', from, entry.id, err);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
seen.add(entry.id);
|
|
317
|
+
out.push({ id: entry.id, record: entry.record });
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
254
321
|
/**
|
|
255
322
|
* The sparsity sample, truncated then vetted per entry. A `coord` is checked by *decoding* it
|
|
256
323
|
* through {@link base64urlToCoord}, which already rejects anything that is not exactly 32 bytes —
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
2
|
+
import type { Libp2p } from 'libp2p';
|
|
3
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
4
|
+
import { type SerializedPeerEntry } from '../store/digitree-store.js';
|
|
5
|
+
/**
|
|
6
|
+
* Signed address records — what FRET carries in a snapshot's `hints` so a peer known only
|
|
7
|
+
* through gossip can still be dialed.
|
|
8
|
+
*
|
|
9
|
+
* The record is libp2p's own: a `RecordEnvelope` (signed, domain-separated) over a `PeerRecord`
|
|
10
|
+
* (`peerId`, `multiaddrs`, `seqNumber`). Verification is `peerStore.consumePeerRecord`, which
|
|
11
|
+
* checks the signature and the signer, refuses a sequence number not newer than the one it holds,
|
|
12
|
+
* and replaces the peer's addresses with the record's — the same code path identify feeds. What
|
|
13
|
+
* this module adds is the part libp2p leaves to the caller: a cheap structural peek so the common
|
|
14
|
+
* path (a neighbour re-sending records we already hold) never reaches the signature check, and a
|
|
15
|
+
* sealer for *this* node's record, since libp2p keeps no self record in the peerStore (identify
|
|
16
|
+
* seals one per request and discards it).
|
|
17
|
+
*/
|
|
18
|
+
/** How many of this node's own addresses a self record carries — bounds the record's size. */
|
|
19
|
+
export declare const MAX_SELF_RECORD_ADDRS = 8;
|
|
20
|
+
/**
|
|
21
|
+
* How long a record for a peer we are **not** connected to keeps being forwarded after we last
|
|
22
|
+
* accepted it. Measured on our own clock (`PeerEntry.addressRecord.confirmedAt`) — never on a
|
|
23
|
+
* timestamp a remote supplied — so clock skew between peers cannot extend it. A stale record
|
|
24
|
+
* cannot be refreshed by re-hearing it either (an equal sequence number is refused), so a record
|
|
25
|
+
* keeps circulating only while some node is connected to its subject or the subject re-seals.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ADDRESS_HINT_FORWARD_MAX_AGE_MS: number;
|
|
28
|
+
/**
|
|
29
|
+
* How long a peer's record survives in an exported routing table, measured from when we last
|
|
30
|
+
* confirmed it (a peer connected at export time counts as confirmed at that moment). Older
|
|
31
|
+
* records are left out of the export and dropped on import.
|
|
32
|
+
*
|
|
33
|
+
* Far longer than the forwarding window, because the two answer different questions: forwarding
|
|
34
|
+
* vouches for a record to *other* peers, while a persisted record only seeds *our own* first
|
|
35
|
+
* dials after a restart — the case where nothing else knows the address at all (gotchoices/sereus#18).
|
|
36
|
+
* The cost of keeping one too long is a dead address, and a failed relayed dial costs seconds
|
|
37
|
+
* (Sereus measured up to 14 s at a 3 s link round trip), so every stale record slows a restart;
|
|
38
|
+
* 14 days still covers a node switched off for a holiday. No separate count cap: at most one
|
|
39
|
+
* record rides on each routing-table entry, so the table's capacity already bounds them.
|
|
40
|
+
*/
|
|
41
|
+
export declare const ADDRESS_RECORD_PERSIST_MAX_AGE_MS: number;
|
|
42
|
+
/**
|
|
43
|
+
* The export half of the persist-age rule, for one entry the store serialized: a peer connected
|
|
44
|
+
* at export time is confirmed *now* (a live connection is the confirmation), and a record last
|
|
45
|
+
* confirmed more than {@link ADDRESS_RECORD_PERSIST_MAX_AGE_MS} ago is left out.
|
|
46
|
+
*/
|
|
47
|
+
export declare function freshenExportedAddressRecord(entry: SerializedPeerEntry, now: number, isConnected: (id: string) => boolean): SerializedPeerEntry;
|
|
48
|
+
/**
|
|
49
|
+
* The import-time check of one persisted entry's address record — structural, no signature
|
|
50
|
+
* check, run over the whole table before anything is written. The signature is verified once,
|
|
51
|
+
* later, inside `consumePeerRecord`.
|
|
52
|
+
*
|
|
53
|
+
* - **Corrupt** → throws, so the caller refuses the whole table: an envelope that is not base64url
|
|
54
|
+
* or does not decode as a signed peer record, a `confirmedAt` that is not a finite number, or a
|
|
55
|
+
* signer or payload peer id that is not the entry's own id. Our own export cannot produce any of
|
|
56
|
+
* these, so the file was damaged.
|
|
57
|
+
* - **Stale** (last confirmed more than {@link ADDRESS_RECORD_PERSIST_MAX_AGE_MS} ago) → the record
|
|
58
|
+
* is dropped and the entry kept. Age is not damage.
|
|
59
|
+
* - A `confirmedAt` later than `now` is clamped to `now`: our clock went backwards since the
|
|
60
|
+
* export, or the file was edited — and an edited file is the table-signing work's concern
|
|
61
|
+
* (`tickets/backlog/23-debt-routing-table-export-signing.md`), not something an age check can see.
|
|
62
|
+
*/
|
|
63
|
+
export declare function vetPersistedAddressRecord(entry: SerializedPeerEntry, now: number): SerializedPeerEntry;
|
|
64
|
+
/** The base64url string a record travels as on the wire. */
|
|
65
|
+
export declare function encodeAddressRecord(envelope: Uint8Array): string;
|
|
66
|
+
/** Inverse of {@link encodeAddressRecord}. Throws on a string that is not base64url. */
|
|
67
|
+
export declare function decodeAddressRecord(record: string): Uint8Array;
|
|
68
|
+
/** What {@link peekPeerRecord} reads off an envelope without verifying it. */
|
|
69
|
+
export interface PeekedPeerRecord {
|
|
70
|
+
/** Peer id derived from the envelope's public key — who signed it. */
|
|
71
|
+
signer: string;
|
|
72
|
+
/** Peer id the payload names — whose addresses it claims to be. */
|
|
73
|
+
peerId: string;
|
|
74
|
+
seq: bigint;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decode an envelope and its peer record **structurally** — no signature check, no domain check,
|
|
78
|
+
* never throws. `undefined` means the bytes do not parse as an envelope over a peer record.
|
|
79
|
+
*
|
|
80
|
+
* Two uses. The identity pre-check: a receiver must refuse a record whose `signer` or `peerId`
|
|
81
|
+
* is not the id it was labelled with *before* it reaches `consumePeerRecord`, because that call
|
|
82
|
+
* checks only the signer against `expectedPeer` and then patches the *payload's* peer id blindly —
|
|
83
|
+
* so a peer could sign a record that rewrites another peer's addresses. And the sequence pre-check:
|
|
84
|
+
* comparing `seq` against the record already held is what keeps a re-sent record free of crypto.
|
|
85
|
+
*/
|
|
86
|
+
export declare function peekPeerRecord(bytes: Uint8Array): PeekedPeerRecord | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* This node's addresses as its self record should state them.
|
|
89
|
+
*
|
|
90
|
+
* Projected the way identify does — `decapsulateCode(CODE_P2P)` strips the trailing `/p2p/<self>`
|
|
91
|
+
* the address manager appends, since a `PeerRecord` names its peer once, in `peerId` — deduped,
|
|
92
|
+
* and then two rules of our own:
|
|
93
|
+
*
|
|
94
|
+
* - A circuit address with **no relay hop** (the bare `/p2p-circuit` listen placeholder, reported
|
|
95
|
+
* from start-up until a reservation lands) is dropped: nobody can dial it.
|
|
96
|
+
* - **Reserved** circuit addresses (`…/p2p/<relay>/p2p-circuit`) come first. For a NAT-only peer
|
|
97
|
+
* they are the only addresses a third party can use, so they must survive the cap.
|
|
98
|
+
*
|
|
99
|
+
* Nothing else is filtered — no public/private test. The address manager already applied the
|
|
100
|
+
* host's announce / noAnnounce configuration, and a private address is harmless in a record
|
|
101
|
+
* (a dial to it fails fast) where a missing circuit address is the whole bug this exists to fix.
|
|
102
|
+
* Capped at {@link MAX_SELF_RECORD_ADDRS}.
|
|
103
|
+
*/
|
|
104
|
+
export declare function selfRecordAddrs(node: Libp2p): Multiaddr[];
|
|
105
|
+
/**
|
|
106
|
+
* This node's own signed address record, sealed lazily and cached until its address list changes.
|
|
107
|
+
*
|
|
108
|
+
* Lazy rather than event-driven on purpose: a snapshot build asks {@link current}, which compares
|
|
109
|
+
* the projected address list against the one last sealed and re-seals only on a difference. That
|
|
110
|
+
* catches a reservation landing after `start()` (the next snapshot carries a fresher record with a
|
|
111
|
+
* higher sequence number, which receivers accept over the earlier one) without a listener that
|
|
112
|
+
* `stop()` would have to detach.
|
|
113
|
+
*/
|
|
114
|
+
export declare class SelfAddressRecord {
|
|
115
|
+
/** The address list the cached record describes, as one string. `''` = nothing sealed yet
|
|
116
|
+
* — which is also the key of an empty address list, and both mean "no record". */
|
|
117
|
+
private sealedFor;
|
|
118
|
+
private cached;
|
|
119
|
+
private lastSeq;
|
|
120
|
+
/**
|
|
121
|
+
* Bumped at the start of every seal. A seal that finds it changed after its signature
|
|
122
|
+
* returns was overtaken by a seal for a *newer* address list, so it must not commit: two
|
|
123
|
+
* snapshot builds straddling a reservation landing would otherwise let the earlier (lower
|
|
124
|
+
* sequence number) record land in the cache last and ride the next snapshot.
|
|
125
|
+
*/
|
|
126
|
+
private generation;
|
|
127
|
+
/** A seal in progress, so two concurrent snapshot builds share one signature. */
|
|
128
|
+
private inflight;
|
|
129
|
+
/**
|
|
130
|
+
* The base64url record for `node`'s current addresses, or `undefined` when there is nothing
|
|
131
|
+
* to advertise: no dialable address yet (a relay-only node before its reservation lands), or a
|
|
132
|
+
* record that would not fit {@link MAX_ADDRESS_RECORD_CHARS}.
|
|
133
|
+
*/
|
|
134
|
+
current(node: Libp2p, privateKey: PrivateKey): Promise<string | undefined>;
|
|
135
|
+
private seal;
|
|
136
|
+
}
|