@serfab/cadre-core 1.6.0 → 1.7.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/cadre-node.d.ts +204 -65
- package/dist/cadre-node.js +392 -95
- package/dist/cadre-node.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +112 -0
- package/dist/joined-strand-store.js +263 -0
- package/dist/joined-strand-store.js.map +1 -0
- package/dist/key-store.d.ts +6 -0
- package/dist/key-store.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +5 -4
- package/dist/node-local-snapshot.js +6 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +33 -14
- package/dist/peer-join-backfill.js +109 -50
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +39 -0
- package/dist/strand-instance-manager.js +74 -1
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-peer-book-file.d.ts +12 -0
- package/dist/strand-peer-book-file.js +37 -0
- package/dist/strand-peer-book-file.js.map +1 -0
- package/dist/strand-peer-book-protocol.d.ts +159 -0
- package/dist/strand-peer-book-protocol.js +360 -0
- package/dist/strand-peer-book-protocol.js.map +1 -0
- package/dist/strand-peer-book-swap.d.ts +104 -0
- package/dist/strand-peer-book-swap.js +284 -0
- package/dist/strand-peer-book-swap.js.map +1 -0
- package/dist/strand-peer-book.d.ts +159 -0
- package/dist/strand-peer-book.js +354 -0
- package/dist/strand-peer-book.js.map +1 -0
- package/dist/strand-peer-observer.d.ts +88 -0
- package/dist/strand-peer-observer.js +180 -0
- package/dist/strand-peer-observer.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -2
- package/src/cadre-node.ts +7776 -7449
- package/src/index.ts +67 -0
- package/src/joined-strand-store.ts +308 -0
- package/src/key-store.ts +6 -0
- package/src/node-local-snapshot.ts +293 -289
- package/src/peer-join-backfill.ts +112 -51
- package/src/strand-instance-manager.ts +100 -1
- package/src/strand-peer-book-file.ts +39 -0
- package/src/strand-peer-book-protocol.ts +463 -0
- package/src/strand-peer-book-swap.ts +338 -0
- package/src/strand-peer-book.ts +468 -0
- package/src/strand-peer-observer.ts +235 -0
- package/src/types.ts +60 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strand peer book SWAP protocol, `/sereus/strand-peers/1.0.0`, on the STRAND
|
|
3
|
+
* libp2p node: when two strand peers connect, each sends the other the signed
|
|
4
|
+
* entries of its strand peer book (`strand-peer-book.ts`) — its own current
|
|
5
|
+
* addresses, self-signed, plus the freshest signed entry it holds for every other
|
|
6
|
+
* peer of the strand — and merges what comes back. That closes the two gaps the
|
|
7
|
+
* node-local book alone leaves: a peer whose relay reservation or address ROTATES is
|
|
8
|
+
* known to the others by its old address only until they happen to reconnect, and in
|
|
9
|
+
* a strand of three or more parties a late joiner holds an address only for the party
|
|
10
|
+
* that invited it. After one swap it holds everyone's.
|
|
11
|
+
*
|
|
12
|
+
* Wire shape, modeled on `strand-addr-protocol.ts`: 4-byte big-endian length-prefixed
|
|
13
|
+
* JSON frames over one stream, one request → one response, the shared
|
|
14
|
+
* `control-stream.ts` primitives, a read timeout and a concurrency cap on the
|
|
15
|
+
* receiver, and a {@link MAX_BOOK_FRAME_SIZE} cap on what either side buffers.
|
|
16
|
+
* Request and response are the same {@link StrandPeerBookFrame}. Only SIGNED entries
|
|
17
|
+
* ever travel: an entry this node observed or a formation carried is its own
|
|
18
|
+
* observation, not the peer's statement, and is never forwarded.
|
|
19
|
+
*
|
|
20
|
+
* ── Whose signature ──
|
|
21
|
+
*
|
|
22
|
+
* Each entry is signed by the named peer's own STRAND TRANSPORT key
|
|
23
|
+
* (`strand-transport-key.ts`, the key its strand libp2p node runs under), over the
|
|
24
|
+
* canonical JSON of `{ v: 1, strandId, peerId, addrs, issuedAt }`. Verification needs
|
|
25
|
+
* no key exchange: an Ed25519 libp2p peer id embeds its public key, so any receiver
|
|
26
|
+
* checks that the entry for peer X was signed by X. NOT the strand member key, for
|
|
27
|
+
* four reasons:
|
|
28
|
+
*
|
|
29
|
+
* 1. Open strands have no shared member key at all, and the book has to work there.
|
|
30
|
+
* 2. A member-key signature proves only "some member said this", so any member could
|
|
31
|
+
* forge another member's address and cost every peer a 14 s relayed dial per
|
|
32
|
+
* launch; a self-signature is a claim only the named peer can make.
|
|
33
|
+
* 3. On a closed strand the member key is shared by every machine of a party, so it
|
|
34
|
+
* cannot identify a machine anyway.
|
|
35
|
+
* 4. Membership is a separate question, judged at the connection by the revocation
|
|
36
|
+
* gate today (`strand-revocation-enforcer.ts`) and by the allowlist later
|
|
37
|
+
* (`feat-strand-member-allowlist-admission`); this protocol rides that gate
|
|
38
|
+
* (`node.handle` → `authorizeInboundStream` on a closed strand).
|
|
39
|
+
*
|
|
40
|
+
* **The book therefore proves "this peer's own claim about where it is", never "this
|
|
41
|
+
* peer is a member of the strand".** An address grants no authority (the dialed peer
|
|
42
|
+
* authenticates by peer id at the handshake), so the worst a forged or stale entry
|
|
43
|
+
* costs is a failed dial, and a self-signature is exactly what bounds who can impose
|
|
44
|
+
* that cost on whom.
|
|
45
|
+
*
|
|
46
|
+
* ── Clocks ──
|
|
47
|
+
*
|
|
48
|
+
* `issuedAt` is the signer's clock. It is compared only between entries from the
|
|
49
|
+
* SAME signer (the store's merge rule), so skew between machines is harmless — except
|
|
50
|
+
* that a far-future stamp would pin an entry forever, hence the
|
|
51
|
+
* {@link STRAND_PEER_ISSUED_AT_SKEW_MS} ceiling (reject and log). The store ages an
|
|
52
|
+
* entry from `max(issuedAt, lastSeenAt)`, so a signer far in the past ages out fast,
|
|
53
|
+
* which is the right failure for a machine with a broken clock.
|
|
54
|
+
*/
|
|
55
|
+
import debug from 'debug';
|
|
56
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
57
|
+
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
58
|
+
import { canonicalJson } from './canonical-json.js';
|
|
59
|
+
import { exchangeFrame, readStreamToEnd, withDeadline, writeFrame } from './control-stream.js';
|
|
60
|
+
import { decodeLengthPrefixedFrame } from './seed-bootstrap.js';
|
|
61
|
+
import { groupAddrsByPeerId } from './peer-addr-book.js';
|
|
62
|
+
import { MAX_STRAND_ADDRS } from './strand-formation-protocol.js';
|
|
63
|
+
import { MAX_STRAND_PEERS } from './strand-peer-book.js';
|
|
64
|
+
const log = debug('sereus:cadre:strand-peer-book-protocol');
|
|
65
|
+
/** Protocol id of the strand peer book swap, registered on every running strand node. */
|
|
66
|
+
export const STRAND_PEER_BOOK_PROTOCOL = '/sereus/strand-peers/1.0.0';
|
|
67
|
+
/** Version tag inside every signed payload, so a later payload shape cannot verify as this one. */
|
|
68
|
+
export const SIGNED_STRAND_PEER_ENTRY_VERSION = 1;
|
|
69
|
+
/**
|
|
70
|
+
* How far ahead of the receiver's clock a signed entry's `issuedAt` may be. Skew
|
|
71
|
+
* between honest machines is seconds; a stamp further ahead than this is a broken
|
|
72
|
+
* clock or an attempt to pin an entry, and is rejected rather than held.
|
|
73
|
+
*/
|
|
74
|
+
export const STRAND_PEER_ISSUED_AT_SKEW_MS = 5 * 60 * 1000;
|
|
75
|
+
/**
|
|
76
|
+
* Cap on one frame's bytes. A typical full frame — {@link MAX_STRAND_PEERS} entries of
|
|
77
|
+
* {@link MAX_STRAND_ADDRS} addresses at roughly 150 bytes each, plus a peer id and
|
|
78
|
+
* signature per entry — is about 40 KiB, but nothing bounds a multiaddr's length (a
|
|
79
|
+
* long DNS name can take one past 300 bytes), so a sender trims its forwarded
|
|
80
|
+
* entries to fit ({@link trimBookFrameToFit}) rather than trusting the arithmetic;
|
|
81
|
+
* {@link assertBookFrameFits} is then the invariant check.
|
|
82
|
+
*/
|
|
83
|
+
export const MAX_BOOK_FRAME_SIZE = 64 * 1024;
|
|
84
|
+
/** Default time the receiver waits for the inbound request frame before aborting (ms). */
|
|
85
|
+
const DEFAULT_READ_TIMEOUT_MS = 10000;
|
|
86
|
+
/** Default time the client waits for the whole exchange (ms). */
|
|
87
|
+
const DEFAULT_EXCHANGE_TIMEOUT_MS = 10000;
|
|
88
|
+
/** Default cap on concurrent inbound swap streams a single strand node serves. */
|
|
89
|
+
const DEFAULT_MAX_CONCURRENT = 100;
|
|
90
|
+
/** The bytes a signed entry's signature covers. */
|
|
91
|
+
export function signedStrandPeerEntryPayload(strandId, entry) {
|
|
92
|
+
return new TextEncoder().encode(canonicalJson({
|
|
93
|
+
v: SIGNED_STRAND_PEER_ENTRY_VERSION,
|
|
94
|
+
strandId,
|
|
95
|
+
peerId: entry.peerId,
|
|
96
|
+
addrs: entry.addrs,
|
|
97
|
+
issuedAt: entry.issuedAt
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Sign this node's own entry with its strand transport key. `addrs` must already be
|
|
102
|
+
* bound to the key's peer id (`dialableAddrs` in `strand-peer-observer.ts` does that);
|
|
103
|
+
* a receiver rejects an entry whose addresses do not all attribute to the signer.
|
|
104
|
+
*/
|
|
105
|
+
export async function signStrandPeerEntry(privateKey, strandId, addrs, issuedAt) {
|
|
106
|
+
if (privateKey.type !== 'Ed25519') {
|
|
107
|
+
throw new Error(`signStrandPeerEntry requires an Ed25519 key, got ${privateKey.type}`);
|
|
108
|
+
}
|
|
109
|
+
const peerId = privateKey.publicKey.toString();
|
|
110
|
+
const entry = { peerId, addrs: [...addrs], issuedAt };
|
|
111
|
+
const sig = await privateKey.sign(signedStrandPeerEntryPayload(strandId, entry));
|
|
112
|
+
return { ...entry, sig: uint8ArrayToString(sig, 'base64url') };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Verify one received entry: well-formed, an Ed25519 peer id, every address bound to
|
|
116
|
+
* that peer id, `issuedAt` not further ahead of `now` than the skew ceiling, and a
|
|
117
|
+
* valid self-signature. Never throws: junk from a peer is a verdict, not an error.
|
|
118
|
+
*/
|
|
119
|
+
export async function verifySignedStrandPeerEntry(strandId, raw, now) {
|
|
120
|
+
const shaped = shapeEntry(raw);
|
|
121
|
+
if (typeof shaped === 'string') {
|
|
122
|
+
return { ok: false, reason: shaped };
|
|
123
|
+
}
|
|
124
|
+
let publicKey;
|
|
125
|
+
try {
|
|
126
|
+
const peerId = peerIdFromString(shaped.peerId);
|
|
127
|
+
if (peerId.type !== 'Ed25519') {
|
|
128
|
+
return { ok: false, reason: `peer id ${shaped.peerId} is ${peerId.type}, not Ed25519` };
|
|
129
|
+
}
|
|
130
|
+
publicKey = peerId.publicKey;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { ok: false, reason: `peer id ${shaped.peerId} does not parse: ${error.message}` };
|
|
134
|
+
}
|
|
135
|
+
const attributed = groupAddrsByPeerId([...shaped.addrs]).get(shaped.peerId)?.length ?? 0;
|
|
136
|
+
if (attributed !== shaped.addrs.length) {
|
|
137
|
+
return { ok: false, reason: `${shaped.addrs.length - attributed} addr(s) do not attribute to ${shaped.peerId}` };
|
|
138
|
+
}
|
|
139
|
+
if (shaped.issuedAt > now + STRAND_PEER_ISSUED_AT_SKEW_MS) {
|
|
140
|
+
return { ok: false, reason: `issuedAt ${shaped.issuedAt} is more than ${STRAND_PEER_ISSUED_AT_SKEW_MS} ms ahead of now` };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const sig = uint8ArrayFromString(shaped.sig, 'base64url');
|
|
144
|
+
if (!(await publicKey.verify(signedStrandPeerEntryPayload(strandId, shaped), sig))) {
|
|
145
|
+
return { ok: false, reason: `signature does not verify for ${shaped.peerId}` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return { ok: false, reason: `signature unusable for ${shaped.peerId}: ${error.message}` };
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, entry: shaped };
|
|
152
|
+
}
|
|
153
|
+
/** Structural check of untrusted JSON: the fields, their types, the address cap. */
|
|
154
|
+
function shapeEntry(raw) {
|
|
155
|
+
if (typeof raw !== 'object' || raw === null)
|
|
156
|
+
return 'entry is not an object';
|
|
157
|
+
const { peerId, addrs, issuedAt, sig } = raw;
|
|
158
|
+
if (typeof peerId !== 'string' || peerId.length === 0)
|
|
159
|
+
return 'peerId is not a string';
|
|
160
|
+
if (!Array.isArray(addrs) || addrs.some((addr) => typeof addr !== 'string' || addr.length === 0)) {
|
|
161
|
+
return `addrs of ${peerId} is not a list of strings`;
|
|
162
|
+
}
|
|
163
|
+
if (addrs.length > MAX_STRAND_ADDRS)
|
|
164
|
+
return `${peerId} lists ${addrs.length} addrs, over the cap of ${MAX_STRAND_ADDRS}`;
|
|
165
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 0)
|
|
166
|
+
return `issuedAt of ${peerId} is not a timestamp`;
|
|
167
|
+
if (typeof sig !== 'string' || sig.length === 0)
|
|
168
|
+
return `sig of ${peerId} is not a string`;
|
|
169
|
+
// A duplicate address is not a protocol error, but the store de-duplicates and the
|
|
170
|
+
// attribution count above compares lengths, so collapse it here.
|
|
171
|
+
return { peerId, addrs: [...new Set(addrs)], issuedAt: issuedAt, sig };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Verify a whole received frame. A frame-level violation — the wrong strand, entries
|
|
175
|
+
* not a list, more entries than {@link MAX_STRAND_PEERS} — throws, and the caller
|
|
176
|
+
* refuses the frame. An entry that fails {@link verifySignedStrandPeerEntry} is
|
|
177
|
+
* dropped INDIVIDUALLY with a debug line; its siblings are kept. An entry naming
|
|
178
|
+
* `selfPeerId` is dropped too: this node's own statement is authoritative locally
|
|
179
|
+
* and a remote copy of it is at best redundant. One entry per peer id (the first).
|
|
180
|
+
*/
|
|
181
|
+
export async function verifyStrandPeerBookFrame(strandId, raw, selfPeerId, now) {
|
|
182
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
183
|
+
throw new Error('strand peer book frame is not an object');
|
|
184
|
+
}
|
|
185
|
+
const { strandId: named, entries } = raw;
|
|
186
|
+
if (named !== strandId) {
|
|
187
|
+
throw new Error(`strand peer book frame names strand ${String(named)}, expected ${strandId}`);
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(entries)) {
|
|
190
|
+
throw new Error('strand peer book frame entries is not a list');
|
|
191
|
+
}
|
|
192
|
+
if (entries.length > MAX_STRAND_PEERS) {
|
|
193
|
+
throw new Error(`strand peer book frame carries ${entries.length} entries, over the cap of ${MAX_STRAND_PEERS}`);
|
|
194
|
+
}
|
|
195
|
+
const verified = new Map();
|
|
196
|
+
for (const candidate of entries) {
|
|
197
|
+
const verdict = await verifySignedStrandPeerEntry(strandId, candidate, now);
|
|
198
|
+
if (!verdict.ok) {
|
|
199
|
+
log('strand %s: dropping received entry: %s', strandId, verdict.reason);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (verdict.entry.peerId === selfPeerId || verified.has(verdict.entry.peerId)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
verified.set(verdict.entry.peerId, verdict.entry);
|
|
206
|
+
}
|
|
207
|
+
return [...verified.values()];
|
|
208
|
+
}
|
|
209
|
+
/** The encoded size of a frame, as {@link writeFrame} would send it (the 4-byte prefix aside). */
|
|
210
|
+
function encodedFrameBytes(frame) {
|
|
211
|
+
return new TextEncoder().encode(JSON.stringify(frame)).length;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The frame with its LAST entries dropped until it encodes within
|
|
215
|
+
* {@link MAX_BOOK_FRAME_SIZE} and {@link MAX_STRAND_PEERS}. Callers list the entries
|
|
216
|
+
* in the order they would rather keep them (the own entry first, then freshest
|
|
217
|
+
* first), so what goes is the stalest forwarded statement, and a frame that fits is
|
|
218
|
+
* returned as is. Dropping an entry is logged: a forwarded peer the recipient never
|
|
219
|
+
* learns of is worth seeing in a trace.
|
|
220
|
+
*/
|
|
221
|
+
export function trimBookFrameToFit(frame) {
|
|
222
|
+
let entries = frame.entries.slice(0, MAX_STRAND_PEERS);
|
|
223
|
+
while (entries.length > 0 && encodedFrameBytes({ strandId: frame.strandId, entries }) > MAX_BOOK_FRAME_SIZE) {
|
|
224
|
+
entries = entries.slice(0, -1);
|
|
225
|
+
}
|
|
226
|
+
if (entries.length === frame.entries.length)
|
|
227
|
+
return frame;
|
|
228
|
+
log('strand %s: frame trimmed from %d to %d entr(ies) to fit the caps', frame.strandId, frame.entries.length, entries.length);
|
|
229
|
+
return { strandId: frame.strandId, entries };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Refuse to send a frame the receiver would reject: over the entry cap, or over
|
|
233
|
+
* {@link MAX_BOOK_FRAME_SIZE} once encoded. A caller that built its frame through
|
|
234
|
+
* {@link trimBookFrameToFit} cannot trip this; it is the invariant check, not a
|
|
235
|
+
* runtime condition to handle.
|
|
236
|
+
*/
|
|
237
|
+
export function assertBookFrameFits(frame) {
|
|
238
|
+
if (frame.entries.length > MAX_STRAND_PEERS) {
|
|
239
|
+
throw new Error(`strand peer book frame carries ${frame.entries.length} entries, over the cap of ${MAX_STRAND_PEERS}`);
|
|
240
|
+
}
|
|
241
|
+
const bytes = encodedFrameBytes(frame);
|
|
242
|
+
if (bytes > MAX_BOOK_FRAME_SIZE) {
|
|
243
|
+
throw new Error(`strand peer book frame is ${bytes} bytes, over the cap of ${MAX_BOOK_FRAME_SIZE}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Read a stream to EOF and decode its single frame, bounded by `timeoutMs` and the size cap. */
|
|
247
|
+
async function readBookFrame(stream, timeoutMs) {
|
|
248
|
+
const data = await readStreamToEnd(stream, { maxBytes: MAX_BOOK_FRAME_SIZE, timeoutMs, label: 'Strand peer book' });
|
|
249
|
+
return JSON.parse(new TextDecoder().decode(decodeLengthPrefixedFrame(data, MAX_BOOK_FRAME_SIZE)));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Receiver side: registers the protocol on a strand node and, per inbound frame,
|
|
253
|
+
* verifies and hands over the asker's entries, then answers with the local book.
|
|
254
|
+
* Registered through `node.handle`, so on a closed strand every inbound stream
|
|
255
|
+
* passes the revocation gate first — a revoked machine cannot push addresses into
|
|
256
|
+
* anyone's book.
|
|
257
|
+
*/
|
|
258
|
+
export class StrandPeerBookService {
|
|
259
|
+
constructor(deps) {
|
|
260
|
+
this.deps = deps;
|
|
261
|
+
this.node = null;
|
|
262
|
+
this.activeStreams = 0;
|
|
263
|
+
this.readTimeoutMs = deps.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
|
|
264
|
+
this.maxConcurrent = deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
265
|
+
this.now = deps.now ?? Date.now;
|
|
266
|
+
}
|
|
267
|
+
/** In-flight inbound streams. */
|
|
268
|
+
get activeCount() {
|
|
269
|
+
return this.activeStreams;
|
|
270
|
+
}
|
|
271
|
+
/** Register the handler. Resolves once libp2p has recorded the protocol on the self record. */
|
|
272
|
+
async initialize(node) {
|
|
273
|
+
this.node = node;
|
|
274
|
+
// `runOnLimitedConnection: true`: strand peers reach each other over relayed
|
|
275
|
+
// (limited) connections as a matter of course, and this is one small frame each way.
|
|
276
|
+
await node.handle(STRAND_PEER_BOOK_PROTOCOL, (rawStream, rawConnection) => {
|
|
277
|
+
const remotePeerId = rawConnection.remotePeer.toString();
|
|
278
|
+
void this.handleStream(rawStream, remotePeerId);
|
|
279
|
+
}, { runOnLimitedConnection: true });
|
|
280
|
+
log('[%s] registered %s', this.deps.strandId, STRAND_PEER_BOOK_PROTOCOL);
|
|
281
|
+
}
|
|
282
|
+
/** Unregister the handler and release the node reference. */
|
|
283
|
+
async shutdown() {
|
|
284
|
+
if (this.node) {
|
|
285
|
+
await this.node.unhandle(STRAND_PEER_BOOK_PROTOCOL);
|
|
286
|
+
this.node = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Read the request, merge what verifies, answer with the local book. Every failure
|
|
291
|
+
* (cap, malformed or oversized frame, wrong strand, read timeout) answers an empty
|
|
292
|
+
* frame rather than hanging or dropping the stream, so the asker's exchange settles.
|
|
293
|
+
*
|
|
294
|
+
* NOTE: the concurrency cap bounds streams in flight, not frames per peer per unit
|
|
295
|
+
* time — the ten-minute throttle is the CLIENT's, so a connected peer can push a
|
|
296
|
+
* verified frame, and the persist it costs, as fast as it likes; see
|
|
297
|
+
* `backlog/debt-strand-peer-book-remote-write-bounds`.
|
|
298
|
+
*/
|
|
299
|
+
async handleStream(stream, remotePeerId) {
|
|
300
|
+
if (this.activeStreams >= this.maxConcurrent) {
|
|
301
|
+
log('[%s] refusing swap from %s: %d streams at the cap', this.deps.strandId, remotePeerId, this.activeStreams);
|
|
302
|
+
this.reply(stream, []);
|
|
303
|
+
await closeQuietly(stream);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.activeStreams++;
|
|
307
|
+
try {
|
|
308
|
+
const raw = await readBookFrame(stream, this.readTimeoutMs);
|
|
309
|
+
const entries = await verifyStrandPeerBookFrame(this.deps.strandId, raw, this.deps.selfPeerId, this.now());
|
|
310
|
+
log('[%s] swap from %s: %d verified entr(ies) received', this.deps.strandId, remotePeerId, entries.length);
|
|
311
|
+
this.deps.onEntries(entries, remotePeerId);
|
|
312
|
+
this.reply(stream, this.deps.localEntries(remotePeerId));
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
log('[%s] swap from %s failed: %o', this.deps.strandId, remotePeerId, error);
|
|
316
|
+
this.reply(stream, []);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
this.activeStreams--;
|
|
320
|
+
await closeQuietly(stream);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
reply(stream, entries) {
|
|
324
|
+
try {
|
|
325
|
+
const frame = { strandId: this.deps.strandId, entries };
|
|
326
|
+
assertBookFrameFits(frame);
|
|
327
|
+
writeFrame(stream, frame);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
log('[%s] swap reply failed: %o', this.deps.strandId, error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function closeQuietly(stream) {
|
|
335
|
+
try {
|
|
336
|
+
await stream.close();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
log('stream close failed (ignored): %o', error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Client side: open one stream on `connection` — the live connection to the peer,
|
|
344
|
+
* relayed or not — send `request`, and return the peer's VERIFIED entries. Throws on
|
|
345
|
+
* any failure (the peer does not speak the protocol, a deadline, a refused frame);
|
|
346
|
+
* the caller logs and moves on, never retrying before its throttle expires.
|
|
347
|
+
*/
|
|
348
|
+
export async function exchangeStrandPeerBook(connection, request, options) {
|
|
349
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_EXCHANGE_TIMEOUT_MS;
|
|
350
|
+
const remotePeerId = connection.remotePeer.toString();
|
|
351
|
+
assertBookFrameFits(request);
|
|
352
|
+
// One deadline for the stream open and the exchange together: its signal aborts the
|
|
353
|
+
// negotiation and resets a live stream, so neither leaks on a timeout.
|
|
354
|
+
const raw = await withDeadline(timeoutMs, `Strand peer book swap with ${remotePeerId}`, async (signal) => {
|
|
355
|
+
const stream = await connection.newStream(STRAND_PEER_BOOK_PROTOCOL, { runOnLimitedConnection: true, signal });
|
|
356
|
+
return exchangeFrame(stream, signal, request, (s) => readBookFrame(s, timeoutMs), 'Strand peer book swap aborted by timeout');
|
|
357
|
+
});
|
|
358
|
+
return verifyStrandPeerBookFrame(request.strandId, raw, options.selfPeerId, (options.now ?? Date.now)());
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=strand-peer-book-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strand-peer-book-protocol.js","sourceRoot":"","sources":["../src/strand-peer-book-protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Libp2p, PrivateKey } from '@libp2p/interface';
|
|
2
|
+
import type { StrandPeerBookStore } from './strand-peer-book.js';
|
|
3
|
+
import { type SignedStrandPeerEntry } from './strand-peer-book-protocol.js';
|
|
4
|
+
/** Minimum gap between two exchanges with ONE peer, unless this node's own entry was re-signed. */
|
|
5
|
+
export declare const STRAND_PEER_BOOK_SWAP_THROTTLE_MS: number;
|
|
6
|
+
/** How long after the last `self:peer:update` the own entry is rebuilt — the event fires in bursts. */
|
|
7
|
+
export declare const OWN_ENTRY_RESIGN_DEBOUNCE_MS = 1000;
|
|
8
|
+
export interface StrandPeerBookSwapDeps {
|
|
9
|
+
strandId: string;
|
|
10
|
+
/** The strand's libp2p node. */
|
|
11
|
+
libp2p: Libp2p;
|
|
12
|
+
/** This strand network's `/optimystic/<networkName>` prefix — the block-transfer gate. */
|
|
13
|
+
protocolPrefix: string;
|
|
14
|
+
/** The node-local book this strand's entries live in. */
|
|
15
|
+
store: StrandPeerBookStore;
|
|
16
|
+
/**
|
|
17
|
+
* The strand transport key the node runs under. Absent — or not the node's own
|
|
18
|
+
* key — means no own entry is signed; the node still receives and forwards.
|
|
19
|
+
*/
|
|
20
|
+
privateKey?: PrivateKey;
|
|
21
|
+
/** The host's declared link round trip, for the exchange deadline (`link-budget.ts`). */
|
|
22
|
+
linkRoundTripMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface StrandPeerBookSwapOptions {
|
|
25
|
+
/** Default {@link STRAND_PEER_BOOK_SWAP_THROTTLE_MS}. */
|
|
26
|
+
throttleMs?: number;
|
|
27
|
+
/** Default {@link OWN_ENTRY_RESIGN_DEBOUNCE_MS}. */
|
|
28
|
+
debounceMs?: number;
|
|
29
|
+
/** Whole-exchange deadline; default derived from `linkRoundTripMs`. */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Clock, for tests. Default `Date.now`. */
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
export declare class StrandPeerBookSwap {
|
|
35
|
+
private readonly deps;
|
|
36
|
+
private readonly service;
|
|
37
|
+
private readonly selfPeerId;
|
|
38
|
+
private readonly signingKey;
|
|
39
|
+
private readonly throttleMs;
|
|
40
|
+
private readonly debounceMs;
|
|
41
|
+
private readonly timeoutMs;
|
|
42
|
+
private readonly now;
|
|
43
|
+
/** Last exchange per peer id, for the throttle. */
|
|
44
|
+
private readonly exchangedAt;
|
|
45
|
+
/** Peers with an exchange in flight, so identify and a walk cannot double up. */
|
|
46
|
+
private readonly inFlight;
|
|
47
|
+
/**
|
|
48
|
+
* Peers whose in-flight exchange carries a since-replaced own entry: one more
|
|
49
|
+
* exchange follows the moment it settles. Without this a re-sign that lands while
|
|
50
|
+
* the identify exchange is still on the wire — the relay reservation arriving a
|
|
51
|
+
* second after the bootstrap dials do, which is routine — would never reach that
|
|
52
|
+
* peer until the next connection.
|
|
53
|
+
*/
|
|
54
|
+
private readonly resendAfter;
|
|
55
|
+
private own;
|
|
56
|
+
/** The address set the own entry was signed over, canonicalised for comparison. */
|
|
57
|
+
private ownAddrKey;
|
|
58
|
+
/** Serialises own-entry refreshes: a burst of triggers signs at most once per change. */
|
|
59
|
+
private selfChain;
|
|
60
|
+
private debounceTimer;
|
|
61
|
+
private readonly onPeerIdentify;
|
|
62
|
+
private readonly onSelfUpdate;
|
|
63
|
+
private started;
|
|
64
|
+
private stopped;
|
|
65
|
+
constructor(deps: StrandPeerBookSwapDeps, options?: StrandPeerBookSwapOptions);
|
|
66
|
+
/** This node's current signed entry, or `undefined` before the first signing or without a key. */
|
|
67
|
+
get ownEntry(): SignedStrandPeerEntry | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Register the receiver, subscribe, sign the own entry, and exchange with the strand
|
|
70
|
+
* peers already connected — identify for an early peer may have fired before this
|
|
71
|
+
* object existed.
|
|
72
|
+
*/
|
|
73
|
+
start(): void;
|
|
74
|
+
/** Unsubscribe and unregister; an exchange in flight finishes on its own. */
|
|
75
|
+
stop(): Promise<void>;
|
|
76
|
+
/** A strand peer that also speaks the swap; never self. */
|
|
77
|
+
private isSwapPeer;
|
|
78
|
+
private queueSelfRefresh;
|
|
79
|
+
/** Re-sign the own entry if the address set changed, then exchange — unthrottled after a re-sign. */
|
|
80
|
+
private refreshSelf;
|
|
81
|
+
/** Returns whether a new own entry was signed. */
|
|
82
|
+
private refreshOwnEntry;
|
|
83
|
+
/** The greatest `issuedAt` this node has signed: in memory, else the stored own entry's, else 0. */
|
|
84
|
+
private lastOwnIssuedAt;
|
|
85
|
+
/** Exchange with every connected, identified strand peer that speaks the swap. */
|
|
86
|
+
private exchangeWithConnected;
|
|
87
|
+
/**
|
|
88
|
+
* One exchange with one peer over `connection`, throttled unless told otherwise (a
|
|
89
|
+
* re-sign) — in which case an exchange already in flight, which carries the old
|
|
90
|
+
* entry, is followed by one more once it settles. Never throws.
|
|
91
|
+
*/
|
|
92
|
+
private exchangeWith;
|
|
93
|
+
/**
|
|
94
|
+
* What this node sends `remotePeerId`: its own entry first, then every signed entry
|
|
95
|
+
* held for anyone else, freshest first, minus the recipient's own — trimmed from the
|
|
96
|
+
* stale end to the frame's entry and byte caps (the own entry can push the store's
|
|
97
|
+
* cap over by one, and long addresses can push the bytes over).
|
|
98
|
+
*/
|
|
99
|
+
private frameFor;
|
|
100
|
+
/** File verified entries: the sender's own as seen now, a forwarded third party's as never seen. */
|
|
101
|
+
private accept;
|
|
102
|
+
/** Fire-and-log, like every book write: visible synchronously, the promise tracks durability. */
|
|
103
|
+
private remember;
|
|
104
|
+
}
|