@serfab/cadre-core 1.16.0 → 1.18.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 +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -18
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +391 -152
|
@@ -4,12 +4,18 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
4
4
|
// Peer-join whole-store block catch-up, shared by the STRAND networks and the CONTROL
|
|
5
5
|
// network. On a new peer's `peer:identify` result — libp2p's identify protocol runs once per
|
|
6
6
|
// connection and reports the protocols the remote supports — naming this network's own
|
|
7
|
-
// block-transfer protocol, it schedules a
|
|
8
|
-
//
|
|
9
|
-
// committed still ends up physically holding them.
|
|
10
|
-
// of its own; a block committed while its writer was
|
|
11
|
-
// without this (named collection-header blocks are
|
|
12
|
-
// and their revision never moves again — so no later
|
|
7
|
+
// block-transfer protocol, it schedules a push of every committed, materialized block in the
|
|
8
|
+
// local raw store that the peer does not already hold (see "What is pushed" below), so a
|
|
9
|
+
// machine that joined after blocks were committed still ends up physically holding them.
|
|
10
|
+
// Optimystic has no cohort-join catch-up of its own; a block committed while its writer was
|
|
11
|
+
// alone has exactly one holder forever without this (named collection-header blocks are
|
|
12
|
+
// written once, at collection creation, and their revision never moves again — so no later
|
|
13
|
+
// commit ever carries them anywhere).
|
|
14
|
+
//
|
|
15
|
+
// A peer this instance has never started a run for is run at once; later schedules for it wait
|
|
16
|
+
// out `debounceMs`. The joining machine's first-sync gate waits for this push rather than
|
|
17
|
+
// fetching the same blocks one read at a time, so on first contact a settle window only delays
|
|
18
|
+
// the joiner. Identify churn during that first run is replayed once the run ends.
|
|
13
19
|
//
|
|
14
20
|
// A peer whose identify does not name the protocol — a bare circuit relay, a bootstrap node,
|
|
15
21
|
// or (on the control network) a stranger the inbound gate admits without membership — is
|
|
@@ -20,7 +26,7 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
20
26
|
// printing exactly that warning about their own relay, forever, on every reconnect. See
|
|
21
27
|
// {@link speaksBlockTransfer}.
|
|
22
28
|
//
|
|
23
|
-
// NOTE: this
|
|
29
|
+
// NOTE: this offers the WHOLE local store to every newly connected peer, which is right
|
|
24
30
|
// while a network is one party's handful of machines (see docs/architecture.md →
|
|
25
31
|
// Replication cluster size — the control network replicates to the whole party by intent,
|
|
26
32
|
// and "On a strand, the machine count is not a party count"). If these meshes ever get
|
|
@@ -32,10 +38,11 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
32
38
|
// than running two.
|
|
33
39
|
//
|
|
34
40
|
// NOTE: strand nodes pass db-p2p no `kvStore`, so Optimystic's under-replication ledger (the
|
|
35
|
-
// copies a node owes peers) dies at every restart. This module's
|
|
36
|
-
//
|
|
37
|
-
// restart arm)
|
|
38
|
-
// nodes a persistent
|
|
41
|
+
// copies a node owes peers) dies at every restart. This module's push on connect is what
|
|
42
|
+
// delivers a write a machine made alone after it restarts (`joiner-first-commit-over-slow-relay`,
|
|
43
|
+
// restart arm): the peer did not sign that commit, so the block is still offered to it. If this
|
|
44
|
+
// module is narrowed (the cohort filter above) or deleted, give strand nodes a persistent
|
|
45
|
+
// kvStore first.
|
|
39
46
|
//
|
|
40
47
|
// Membership — the one place the two networks differ, expressed as the optional
|
|
41
48
|
// `authorizePeer` dep:
|
|
@@ -50,11 +57,12 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
50
57
|
// invitation, configured bootstrap/relay peers — see docs/architecture.md, the
|
|
51
58
|
// control-network inbound connection gate). Pushing the whole control store to such a
|
|
52
59
|
// peer would hand a stranger the party's entire membership, addresses and strand list.
|
|
53
|
-
// `CadreNode` passes
|
|
54
|
-
// schedule time, and fails closed on a thrown check. A denied
|
|
55
|
-
// the peer is retried on its next `peer:identify` (a reconnect
|
|
56
|
-
// sooner, via `scheduleConnectedPeers()` on a membership change (the
|
|
57
|
-
// connect-then-authorize, so the denial at dial time is the
|
|
60
|
+
// `CadreNode` passes a lookup in its in-memory authorized-member snapshot; the gate is
|
|
61
|
+
// consulted at PUSH time, not at schedule time, and fails closed on a thrown check. A denied
|
|
62
|
+
// run is not memoized, so the peer is retried on its next `peer:identify` (a reconnect
|
|
63
|
+
// re-runs identify) — or sooner, via `scheduleConnectedPeers()` on a membership change (the
|
|
64
|
+
// production join order is connect-then-authorize, so the denial at dial time is the
|
|
65
|
+
// expected first pass).
|
|
58
66
|
//
|
|
59
67
|
// Both per-push deadlines are DERIVED from the declared link round trip
|
|
60
68
|
// (`link-budget.ts`), not fixed milliseconds. That is not tuning — it is what makes this
|
|
@@ -89,21 +97,17 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
89
97
|
// budgets its dial at `transferTimeoutMs ?? 30000` — the one dial budget in the stack
|
|
90
98
|
// above the measured relayed setup cost — so it succeeded where this one could not.
|
|
91
99
|
//
|
|
92
|
-
// A
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// `
|
|
102
|
-
//
|
|
103
|
-
// un-memoized, so its next `peer:identify` retries — the behaviour that predates the
|
|
104
|
-
// backoff, and the right one for a verdict rather than a timeout. Denial in particular is
|
|
105
|
-
// re-driven on purpose by `scheduleConnectedPeers()` the moment the membership commit lands,
|
|
106
|
-
// because the control network's join order is connect-then-authorize.
|
|
100
|
+
// A retry re-sends only the blocks the peer has not yet accepted (see "What is pushed"), so a
|
|
101
|
+
// run that died part-way through the store resumes where it stopped.
|
|
102
|
+
//
|
|
103
|
+
// Only a failed push re-arms. Two other outcomes also leave a run short of catching the peer
|
|
104
|
+
// up, and no amount of retrying changes them: the membership gate DENIED the peer, or this
|
|
105
|
+
// network's raw storage implements no `listBlockIds`, so the catch-up is inert. Re-arming on
|
|
106
|
+
// those would retry every `maxRetryBackoffMs` for as long as the node runs, and report the
|
|
107
|
+
// failure below as a link budget problem when nothing about the link is wrong. Neither marks
|
|
108
|
+
// the peer caught up, so its next `peer:identify` retries. Denial in particular is re-driven on
|
|
109
|
+
// purpose by `scheduleConnectedPeers()` the moment the membership commit lands, because the
|
|
110
|
+
// control network's join order is connect-then-authorize.
|
|
107
111
|
//
|
|
108
112
|
// A re-arm is also skipped when the peer is no longer CONNECTED. This module's trigger is
|
|
109
113
|
// `peer:identify`, so a peer that went away already has one: re-arming instead would leave
|
|
@@ -114,6 +118,43 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
114
118
|
// nothing at all: the connection simply never appears and only a DEBUG log mentions a dial
|
|
115
119
|
// timeout.
|
|
116
120
|
//
|
|
121
|
+
// WHAT IS PUSHED: each block once per peer. Per peer, this module remembers for the runtime the
|
|
122
|
+
// revision of each block the peer accepted and the revision it refused ({@link PeerMemo}). A
|
|
123
|
+
// refusal is the receiver's `missing` list: a payload it cannot parse, a block it failed to
|
|
124
|
+
// persist, or a revision whose retained commit proof this node does not hold (a receiver running the default
|
|
125
|
+
// `requirePushCertificate: true` refuses a push carrying no proof, and an unretained proof is
|
|
126
|
+
// the ordinary case — see {@link Chunk.proofs}). A refusal is a per-block verdict: logged once
|
|
127
|
+
// when it happens, and the block is offered to that peer again only once its local revision
|
|
128
|
+
// moves past the refused one. A run skips a block, counting it in `alreadyHeld`, when:
|
|
129
|
+
//
|
|
130
|
+
// - the peer accepted that revision or a later one from this node;
|
|
131
|
+
// - the peer refused that revision;
|
|
132
|
+
// - the commit proof retained for that revision carries the peer's own commit-round approval.
|
|
133
|
+
// A block's proof lists the cohort members that signed the commit round for exactly that
|
|
134
|
+
// revision, so a founder's solo commit is never pushed back to it by a joiner now holding it,
|
|
135
|
+
// and a revision two machines committed together is not pushed between them.
|
|
136
|
+
//
|
|
137
|
+
// The proof rule stands in for "do not push a block back to the peer it came from": Optimystic
|
|
138
|
+
// reports a push's sender only to its own rebalance monitor and exposes it to no embedder.
|
|
139
|
+
//
|
|
140
|
+
// NOTE: accepted tradeoff — a signer that crashed between signing and applying, or later lost its
|
|
141
|
+
// store, is not re-sent that revision by this path; read repair covers its reads, as for any block
|
|
142
|
+
// a cohort member lacks. Revisit if Optimystic tells embedders who holds what (it takes reports,
|
|
143
|
+
// `reportBlockHeld`, but gives none) or a measurement shows pushes this rule misses.
|
|
144
|
+
//
|
|
145
|
+
// NOTE: the reply does not say why a block is missing, so a receiver's transient persist failure
|
|
146
|
+
// is remembered as a refusal too and not retried this runtime; read repair covers that block's
|
|
147
|
+
// reads. If receivers log `persist:fail` in practice, ask Optimystic for a per-block reason in
|
|
148
|
+
// the push reply and remember only the verdicts a retry cannot change.
|
|
149
|
+
//
|
|
150
|
+
// A peer is marked caught up (`done`) once a run finishes with no push that threw, the gate
|
|
151
|
+
// admitted it and the store is listable. A refusal does not hold that back, since it is
|
|
152
|
+
// remembered per block. It stays caught up until its last connection closes
|
|
153
|
+
// (`peer:disconnect`): `peer:identify` churn for a connected, caught-up peer is dropped before
|
|
154
|
+
// any work, and a reconnect re-runs past the debounce, pushing only what the memo and the
|
|
155
|
+
// proofs do not cover. {@link PeerJoinBackfill.forceCatchUpPeer} runs past `done` too, with the
|
|
156
|
+
// same skips.
|
|
157
|
+
//
|
|
117
158
|
// NOTE: the protocol check happens once — either when a connection's `peer:identify` fires,
|
|
118
159
|
// or, for a peer already connected when this instance starts, when `scheduleConnectedPeers()`
|
|
119
160
|
// reads its protocols back from the peer store — and is never repeated once a peer is
|
|
@@ -132,10 +173,11 @@ import { peerJoinPushBudget } from './link-budget.js';
|
|
|
132
173
|
//
|
|
133
174
|
// NOTE: a peer that disconnects INSIDE a backoff wait still costs the one attempt that wait
|
|
134
175
|
// was already armed for — the connectivity check is made when the re-arm is decided, not when
|
|
135
|
-
// the timer fires, and
|
|
136
|
-
// recurring one; if a node ever holds many transient peers,
|
|
137
|
-
//
|
|
176
|
+
// the timer fires, and the `peer:disconnect` handler leaves backoff state alone (see
|
|
177
|
+
// `forgetCaughtUp`). One dial, not a recurring one; if a node ever holds many transient peers,
|
|
178
|
+
// clear the pending timer in that handler.
|
|
138
179
|
const log = debug('sereus:cadre:peer-join-backfill');
|
|
180
|
+
const blockEncoder = new TextEncoder();
|
|
139
181
|
/**
|
|
140
182
|
* The block-transfer protocol's hard cap on one length-prefixed message. Mirrors
|
|
141
183
|
* `MAX_BLOCK_MESSAGE_BYTES` in `@optimystic/db-p2p`'s `protocol-limits.ts` (8 MiB), which
|
|
@@ -176,7 +218,24 @@ export const DEFAULT_PEER_JOIN_BACKFILL = {
|
|
|
176
218
|
maxRetryBackoffMs: 60000
|
|
177
219
|
};
|
|
178
220
|
function emptyResult() {
|
|
179
|
-
return { offered: 0, accepted: 0, rejected: [], uncommitted: 0, unmaterialized: 0, capped: 0, oversized: [], denied: false };
|
|
221
|
+
return { offered: 0, alreadyHeld: 0, accepted: 0, rejected: [], uncommitted: 0, unmaterialized: 0, capped: 0, oversized: [], denied: false };
|
|
222
|
+
}
|
|
223
|
+
/** Whether the peer's replies already settled revision `rev` of `blockId`, either way. */
|
|
224
|
+
function memoSettled(memo, blockId, rev) {
|
|
225
|
+
return (memo.accepted.get(blockId) ?? -Infinity) >= rev
|
|
226
|
+
|| (memo.rejected.get(blockId) ?? -Infinity) >= rev;
|
|
227
|
+
}
|
|
228
|
+
/** Whether `peerId` approved the commit round that `proof` certifies. */
|
|
229
|
+
function signedBy(proof, peerId) {
|
|
230
|
+
return proof?.commits[peerId]?.type === 'approve';
|
|
231
|
+
}
|
|
232
|
+
/** Record one push reply in the memo: each block as accepted or refused at the revision pushed. */
|
|
233
|
+
function recordReply(memo, revs, ids, missing) {
|
|
234
|
+
for (const id of ids) {
|
|
235
|
+
const rev = revs[id].rev;
|
|
236
|
+
const verdicts = missing.has(id) ? memo.rejected : memo.accepted;
|
|
237
|
+
verdicts.set(id, Math.max(verdicts.get(id) ?? -Infinity, rev));
|
|
238
|
+
}
|
|
180
239
|
}
|
|
181
240
|
/**
|
|
182
241
|
* Base64 wire size of a raw buffer — `pushBlocks` base64-encodes each block into the JSON
|
|
@@ -193,8 +252,8 @@ function base64WireBytes(rawBytes) {
|
|
|
193
252
|
*
|
|
194
253
|
* Best-effort throughout: nothing here throws into a libp2p event handler or into a
|
|
195
254
|
* runtime bring-up; a failed chunk is logged and the run continues. A peer is marked
|
|
196
|
-
*
|
|
197
|
-
*
|
|
255
|
+
* caught up only after a run with no thrown chunk, so a peer whose catch-up failed is
|
|
256
|
+
* retried; each block is pushed to each peer once (see "What is pushed" in the module comment).
|
|
198
257
|
*/
|
|
199
258
|
export class PeerJoinBackfill {
|
|
200
259
|
constructor(deps, config) {
|
|
@@ -202,10 +261,22 @@ export class PeerJoinBackfill {
|
|
|
202
261
|
this.started = false;
|
|
203
262
|
this.stopped = false;
|
|
204
263
|
/**
|
|
205
|
-
* Peers
|
|
206
|
-
* {@link forceCatchUpPeer}
|
|
264
|
+
* Peers caught up since their current connection opened. Cleared for a peer when its last
|
|
265
|
+
* connection closes, so a reconnect re-runs; {@link forceCatchUpPeer} runs past it.
|
|
207
266
|
*/
|
|
208
267
|
this.done = new Set();
|
|
268
|
+
/**
|
|
269
|
+
* Peers that disconnected while their run was in flight, so that run does not mark them
|
|
270
|
+
* {@link done} after the disconnect cleared it. Cleared when the run ends.
|
|
271
|
+
*/
|
|
272
|
+
this.disconnectedInFlight = new Set();
|
|
273
|
+
/** Per-peer, per-block verdicts from push replies; see {@link PeerMemo}. */
|
|
274
|
+
this.memos = new Map();
|
|
275
|
+
/**
|
|
276
|
+
* Peers a run has been started for. A peer not in it is run at once on its first schedule;
|
|
277
|
+
* later schedules wait out `debounceMs`.
|
|
278
|
+
*/
|
|
279
|
+
this.contacted = new Set();
|
|
209
280
|
/**
|
|
210
281
|
* Peers with a catch-up currently running, each with that run (suppresses concurrent
|
|
211
282
|
* duplicates; {@link forceCatchUpPeer} waits on the run).
|
|
@@ -213,9 +284,9 @@ export class PeerJoinBackfill {
|
|
|
213
284
|
this.inFlight = new Map();
|
|
214
285
|
/**
|
|
215
286
|
* Peers whose (re-)schedule arrived while their OWN run was in flight, replayed once
|
|
216
|
-
* that run finishes. Load-bearing for the gated path: the gate
|
|
217
|
-
* a
|
|
218
|
-
*
|
|
287
|
+
* that run finishes. Load-bearing for the gated path: the gate is judged once, at the start
|
|
288
|
+
* of a run, and the run's pushes take long enough for the very membership commit that would
|
|
289
|
+
* have authorized the peer to land after that check —
|
|
219
290
|
* dropping that schedule (rather than deferring it) leaves the denied peer waiting for a
|
|
220
291
|
* reconnect, which is exactly what the re-arm exists to avoid.
|
|
221
292
|
*/
|
|
@@ -242,18 +313,23 @@ export class PeerJoinBackfill {
|
|
|
242
313
|
this.schedulePeer(peerId);
|
|
243
314
|
}
|
|
244
315
|
};
|
|
316
|
+
this.onPeerDisconnect = (evt) => {
|
|
317
|
+
this.forgetCaughtUp(evt.detail.toString());
|
|
318
|
+
};
|
|
245
319
|
}
|
|
246
320
|
/**
|
|
247
321
|
* Subscribe to `peer:identify` AND schedule a catch-up for already-connected peers whose
|
|
248
322
|
* stored protocols already name this network's block-transfer protocol — a peer that does
|
|
249
|
-
* not speak it (a bare circuit relay, a bootstrap node) is never scheduled at all.
|
|
250
|
-
*
|
|
323
|
+
* not speak it (a bare circuit relay, a bootstrap node) is never scheduled at all. Also
|
|
324
|
+
* subscribes to `peer:disconnect`, so a peer caught up before it went away is run again on
|
|
325
|
+
* its return. See {@link speaksBlockTransfer} and the module comment.
|
|
251
326
|
*/
|
|
252
327
|
start() {
|
|
253
328
|
if (this.started || this.stopped || !this.config.enabled)
|
|
254
329
|
return;
|
|
255
330
|
this.started = true;
|
|
256
331
|
this.deps.libp2p.addEventListener('peer:identify', this.onPeerIdentify);
|
|
332
|
+
this.deps.libp2p.addEventListener('peer:disconnect', this.onPeerDisconnect);
|
|
257
333
|
// A runtime rebuilt over live connections (resumeStrand) never sees their peer:identify —
|
|
258
334
|
// it already fired before this instance existed, but its result is persisted in the peer
|
|
259
335
|
// store — so walk what is already connected once.
|
|
@@ -268,6 +344,7 @@ export class PeerJoinBackfill {
|
|
|
268
344
|
this.stopped = true;
|
|
269
345
|
if (this.started) {
|
|
270
346
|
this.deps.libp2p.removeEventListener('peer:identify', this.onPeerIdentify);
|
|
347
|
+
this.deps.libp2p.removeEventListener('peer:disconnect', this.onPeerDisconnect);
|
|
271
348
|
}
|
|
272
349
|
for (const timer of this.timers.values()) {
|
|
273
350
|
clearTimeout(timer);
|
|
@@ -277,6 +354,24 @@ export class PeerJoinBackfill {
|
|
|
277
354
|
this.retryAfter.clear();
|
|
278
355
|
log('[%s] stopped', this.deps.label);
|
|
279
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* A peer's last connection closed: it is no longer caught up, so its next `peer:identify`
|
|
359
|
+
* re-runs. Only `done` is cleared. The memo stays (it keeps the re-run to what moved),
|
|
360
|
+
* `contacted` stays (a link flapping inside one settle window costs one run), and the
|
|
361
|
+
* backoff state stays (clearing it re-dials an unreachable peer on every reconnect; see the
|
|
362
|
+
* module comment's retry paragraph).
|
|
363
|
+
*
|
|
364
|
+
* When the returning peer wrote while away with a cohort that named this node, its own
|
|
365
|
+
* re-run pushes those blocks here and reports them through `reportBlockHeld`, so its
|
|
366
|
+
* under-replication drain does not push them again. Revisit if a measurement shows reconnect
|
|
367
|
+
* re-runs dominating push traffic.
|
|
368
|
+
*/
|
|
369
|
+
forgetCaughtUp(key) {
|
|
370
|
+
this.done.delete(key);
|
|
371
|
+
if (this.inFlight.has(key)) {
|
|
372
|
+
this.disconnectedInFlight.add(key);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
280
375
|
/**
|
|
281
376
|
* (Re-)schedule a debounced catch-up for every currently-connected peer whose protocols, as
|
|
282
377
|
* already stored in the libp2p peer store, name this network's block-transfer protocol. A
|
|
@@ -318,7 +413,11 @@ export class PeerJoinBackfill {
|
|
|
318
413
|
this.schedulePeer(peerId);
|
|
319
414
|
}
|
|
320
415
|
}
|
|
321
|
-
/**
|
|
416
|
+
/**
|
|
417
|
+
* Entry point for connection churn: one run per peer per settle window, except that a peer
|
|
418
|
+
* never run for is run at once (on a zero-delay timer, so a burst of identify events in one
|
|
419
|
+
* turn still collapses, and no work happens inside the libp2p event dispatch).
|
|
420
|
+
*/
|
|
322
421
|
schedulePeer(peerId) {
|
|
323
422
|
const key = peerId.toString();
|
|
324
423
|
if (this.stopped || this.done.has(key))
|
|
@@ -339,10 +438,11 @@ export class PeerJoinBackfill {
|
|
|
339
438
|
const existing = this.timers.get(key);
|
|
340
439
|
if (existing)
|
|
341
440
|
clearTimeout(existing);
|
|
441
|
+
const delayMs = this.contacted.has(key) ? this.config.debounceMs : 0;
|
|
342
442
|
this.timers.set(key, setTimeout(() => {
|
|
343
443
|
this.timers.delete(key);
|
|
344
444
|
void this.catchUpPeer(peerId);
|
|
345
|
-
},
|
|
445
|
+
}, delayMs));
|
|
346
446
|
}
|
|
347
447
|
/**
|
|
348
448
|
* Run one peer's catch-up now, bypassing the debounce. Never rejects. Returns an
|
|
@@ -357,14 +457,16 @@ export class PeerJoinBackfill {
|
|
|
357
457
|
return await this.trackRun(peerId);
|
|
358
458
|
}
|
|
359
459
|
/**
|
|
360
|
-
* Run one peer's catch-up now and wait for it,
|
|
361
|
-
*
|
|
362
|
-
*
|
|
460
|
+
* Run one peer's catch-up now and wait for it, even when the peer is memoized as caught up:
|
|
461
|
+
* wait out a run already in flight for it, then run again. The run still skips every block the
|
|
462
|
+
* peer is known to hold (it accepted or refused that revision, or signed it), so for a peer that
|
|
463
|
+
* holds the whole store it pushes nothing and returns `alreadyHeld` equal to the store size.
|
|
464
|
+
* Never rejects; an all-zero result only when stopped.
|
|
363
465
|
*
|
|
364
466
|
* For a caller that must know the peer holds this store before it proceeds, where
|
|
365
467
|
* {@link catchUpPeer}'s empty answer would hide a run that was denied (a `peer:identify`
|
|
366
|
-
* run can pass the gate a moment before the caller authorized the peer) or
|
|
367
|
-
*
|
|
468
|
+
* run can pass the gate a moment before the caller authorized the peer) or commits made
|
|
469
|
+
* since the peer was caught up.
|
|
368
470
|
*/
|
|
369
471
|
async forceCatchUpPeer(peerId) {
|
|
370
472
|
const key = peerId.toString();
|
|
@@ -382,6 +484,7 @@ export class PeerJoinBackfill {
|
|
|
382
484
|
*/
|
|
383
485
|
async trackRun(peerId) {
|
|
384
486
|
const key = peerId.toString();
|
|
487
|
+
this.contacted.add(key);
|
|
385
488
|
const run = this.settleRun(peerId);
|
|
386
489
|
this.inFlight.set(key, run);
|
|
387
490
|
try {
|
|
@@ -389,6 +492,7 @@ export class PeerJoinBackfill {
|
|
|
389
492
|
}
|
|
390
493
|
finally {
|
|
391
494
|
this.inFlight.delete(key);
|
|
495
|
+
this.disconnectedInFlight.delete(key);
|
|
392
496
|
// Replay a schedule that arrived mid-run. `schedulePeer` re-checks `done`, so a run
|
|
393
497
|
// that finished clean re-arms nothing; a denied or failed one gets its retry.
|
|
394
498
|
if (this.rearmAfterFlight.delete(key)) {
|
|
@@ -401,28 +505,32 @@ export class PeerJoinBackfill {
|
|
|
401
505
|
const key = peerId.toString();
|
|
402
506
|
try {
|
|
403
507
|
const { result, clean, pushFailed } = await this.runCatchUp(peerId);
|
|
404
|
-
// NOTE: a run that hit `maxBlocks` is still "clean" and still
|
|
405
|
-
// the tail past the ceiling
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
// realistically exceed maxBlocks,
|
|
508
|
+
// NOTE: a run that hit `maxBlocks` is still "clean" and still marks the peer caught up,
|
|
509
|
+
// so no `peer:identify` run reaches the tail past the ceiling. Loud in the log below
|
|
510
|
+
// (capped > 0). Blocks the peer is known to hold are not offered and do not count toward
|
|
511
|
+
// the ceiling, so a forced run resumes past the prefix already sent. If a store can
|
|
512
|
+
// realistically exceed maxBlocks, leave a capped run out of `done` so each identify run
|
|
513
|
+
// sends the next `maxBlocks`.
|
|
409
514
|
if (clean && !this.stopped) {
|
|
410
|
-
|
|
515
|
+
// A disconnect mid-run cleared `done` already; re-adding it would drop the reconnect.
|
|
516
|
+
// Not judged by `isConnected`: hand-driven runs target peers with no connection.
|
|
517
|
+
if (!this.disconnectedInFlight.has(key)) {
|
|
518
|
+
this.done.add(key);
|
|
519
|
+
}
|
|
411
520
|
this.failures.delete(key);
|
|
412
521
|
this.retryAfter.delete(key);
|
|
413
522
|
this.warned.delete(key);
|
|
414
523
|
}
|
|
415
524
|
else if (pushFailed && !this.stopped && this.started && this.isConnected(peerId)) {
|
|
416
|
-
// `pushFailed`, not `!clean`: a denial
|
|
417
|
-
//
|
|
418
|
-
// re-push the whole store to that peer forever. See the module comment.
|
|
525
|
+
// `pushFailed`, not `!clean`: a denial and an inert store are verdicts a retry cannot
|
|
526
|
+
// change, and re-arming on them would retry forever. See the module comment.
|
|
419
527
|
//
|
|
420
528
|
// Gated on `started` too: the re-arm exists to REPLACE a peer:identify-driven retry,
|
|
421
529
|
// so a caller driving `catchUpPeer` by hand against a backfill that was never started
|
|
422
530
|
// owns its own retry policy and must not be left holding a background timer.
|
|
423
531
|
this.scheduleRetryWithBackoff(peerId);
|
|
424
532
|
}
|
|
425
|
-
log('[%s] catch-up peer=%s offered=%d accepted=%d rejected=%d uncommitted=%d unmaterialized=%d capped=%d oversized=%d denied=%s done=%s', this.deps.label, key, result.offered, result.accepted, result.rejected.length, result.uncommitted, result.unmaterialized, result.capped, result.oversized.length, result.denied, clean);
|
|
533
|
+
log('[%s] catch-up peer=%s offered=%d alreadyHeld=%d accepted=%d rejected=%d uncommitted=%d unmaterialized=%d capped=%d oversized=%d denied=%s done=%s', this.deps.label, key, result.offered, result.alreadyHeld, result.accepted, result.rejected.length, result.uncommitted, result.unmaterialized, result.capped, result.oversized.length, result.denied, clean);
|
|
426
534
|
return result;
|
|
427
535
|
}
|
|
428
536
|
catch (error) {
|
|
@@ -474,14 +582,44 @@ export class PeerJoinBackfill {
|
|
|
474
582
|
+ 'these budgets and libp2p\'s own connection limits are derived from network.linkRoundTripMs (see link-budget.ts); '
|
|
475
583
|
+ 'a link slower than that declaration, or a peer that declared a faster one, cannot open a relayed connection at all.');
|
|
476
584
|
}
|
|
585
|
+
/** This peer's memo, created empty on first use. */
|
|
586
|
+
memoFor(key) {
|
|
587
|
+
let memo = this.memos.get(key);
|
|
588
|
+
if (!memo) {
|
|
589
|
+
memo = { accepted: new Map(), rejected: new Map() };
|
|
590
|
+
this.memos.set(key, memo);
|
|
591
|
+
}
|
|
592
|
+
return memo;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Tell the node which blocks of one push reply the peer accepted, at the revision pushed.
|
|
596
|
+
* Best-effort: a failed report is logged and costs only the drain's duplicate push.
|
|
597
|
+
*/
|
|
598
|
+
async reportAccepted(key, revs, ids, missing) {
|
|
599
|
+
const report = this.deps.reportBlockHeld;
|
|
600
|
+
if (!report)
|
|
601
|
+
return;
|
|
602
|
+
for (const id of ids) {
|
|
603
|
+
if (missing.has(id))
|
|
604
|
+
continue;
|
|
605
|
+
try {
|
|
606
|
+
await report(id, revs[id].rev, key);
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
log('[%s] reportBlockHeld for peer=%s block=%s failed: %s', this.deps.label, key, id, error.message);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
477
613
|
/**
|
|
478
|
-
* The actual copy. `clean` =
|
|
614
|
+
* The actual copy. `clean` = the gate admitted the peer, the store is listable and no push
|
|
615
|
+
* threw (a block the receiver refused is remembered per block instead, see {@link PeerMemo}).
|
|
479
616
|
* `pushFailed` = at least one push THREW, which is the only non-clean outcome a retry can
|
|
480
|
-
* change; the others are verdicts (denied, inert store
|
|
617
|
+
* change; the others are verdicts (denied, inert store).
|
|
481
618
|
*/
|
|
482
619
|
async runCatchUp(peerId) {
|
|
483
620
|
const result = emptyResult();
|
|
484
621
|
const { storage } = this.deps;
|
|
622
|
+
const key = peerId.toString();
|
|
485
623
|
// The membership gate, judged at push time (never at schedule time — authorization can
|
|
486
624
|
// change during the debounce window in either direction). Fails CLOSED on a throw: a
|
|
487
625
|
// gate that cannot answer must not leak the store. A denied run is not clean, so the
|
|
@@ -507,7 +645,7 @@ export class PeerJoinBackfill {
|
|
|
507
645
|
return { result, clean: false, pushFailed: false };
|
|
508
646
|
}
|
|
509
647
|
const client = this.createPushClient(peerId);
|
|
510
|
-
const
|
|
648
|
+
const memo = this.memoFor(key);
|
|
511
649
|
let chunk = { ids: [], buffers: [], meta: {}, proofs: {}, bytes: 0 };
|
|
512
650
|
let chunkFailed = false;
|
|
513
651
|
let anyChunkDelivered = false;
|
|
@@ -532,6 +670,8 @@ export class PeerJoinBackfill {
|
|
|
532
670
|
});
|
|
533
671
|
anyChunkDelivered = true;
|
|
534
672
|
const missing = new Set(response.missing);
|
|
673
|
+
recordReply(memo, meta, ids, missing);
|
|
674
|
+
await this.reportAccepted(key, meta, ids, missing);
|
|
535
675
|
for (const id of ids) {
|
|
536
676
|
if (missing.has(id)) {
|
|
537
677
|
result.rejected.push(id);
|
|
@@ -541,12 +681,12 @@ export class PeerJoinBackfill {
|
|
|
541
681
|
}
|
|
542
682
|
}
|
|
543
683
|
if (response.missing.length > 0) {
|
|
544
|
-
log('[%s] peer=%s rejected %d block(s): %o', this.deps.label,
|
|
684
|
+
log('[%s] peer=%s rejected %d block(s), not offered to it again at these revisions: %o', this.deps.label, key, response.missing.length, response.missing);
|
|
545
685
|
}
|
|
546
686
|
}
|
|
547
687
|
catch (error) {
|
|
548
688
|
chunkFailed = true;
|
|
549
|
-
log('[%s] push to peer=%s failed for %d block(s): %s', this.deps.label,
|
|
689
|
+
log('[%s] push to peer=%s failed for %d block(s): %s', this.deps.label, key, ids.length, error.message);
|
|
550
690
|
}
|
|
551
691
|
};
|
|
552
692
|
for await (const blockId of storage.listBlockIds()) {
|
|
@@ -558,65 +698,82 @@ export class PeerJoinBackfill {
|
|
|
558
698
|
result.capped += 1;
|
|
559
699
|
continue;
|
|
560
700
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
if (!latest) {
|
|
564
|
-
result.uncommitted += 1;
|
|
701
|
+
const offer = await this.readOffer(blockId, key, memo, result);
|
|
702
|
+
if (!offer)
|
|
565
703
|
continue;
|
|
566
|
-
}
|
|
567
|
-
// NOTE: a block whose latest revision is a DELETE materializes to nothing (a
|
|
568
|
-
// tombstone), so it is skipped here — a peer that joins after such a delete never
|
|
569
|
-
// receives the tombstone from this path and relies on read repair for it. Fine
|
|
570
|
-
// while nothing deletes whole blocks pre-join; if the whole-store coverage gate in
|
|
571
|
-
// strand-membership-closed-strand-e2e ever reports a tombstone residue, teach this
|
|
572
|
-
// to push the promoted delete transform instead of skipping.
|
|
573
|
-
const block = await storage.getMaterializedBlock(blockId, latest.actionId);
|
|
574
|
-
if (!block) {
|
|
575
|
-
result.unmaterialized += 1;
|
|
576
|
-
log('[%s] block=%s has latest rev %d but no materialized content; skipped', this.deps.label, blockId, latest.rev);
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
const buffer = encoder.encode(JSON.stringify(block));
|
|
580
|
-
// NOTE: MAX_BLOCK_MESSAGE_BYTES caps the whole framed request, not one block, and
|
|
581
|
-
// this test ignores the request envelope (ids array, blockMeta, JSON punctuation).
|
|
582
|
-
// A lone block within a few KiB of the cap therefore still ships and is rejected by
|
|
583
|
-
// the receiver's length-prefix decoder — a permanent chunk failure for that peer. No
|
|
584
|
-
// block comes close today; if one ever can, subtract a measured envelope allowance
|
|
585
|
-
// here rather than raising the cap.
|
|
586
|
-
if (base64WireBytes(buffer.length) >= MAX_BLOCK_MESSAGE_BYTES) {
|
|
587
|
-
result.oversized.push(blockId);
|
|
588
|
-
log('[%s] block=%s is %d bytes (%d on the wire) — exceeds the %d-byte protocol cap alone; skipped', this.deps.label, blockId, buffer.length, base64WireBytes(buffer.length), MAX_BLOCK_MESSAGE_BYTES);
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
704
|
// Flush before adding when this block would overflow either budget. A single block
|
|
592
705
|
// larger than maxChunkBytes still ships — alone, in its own chunk.
|
|
593
706
|
if (chunk.ids.length > 0
|
|
594
|
-
&& (chunk.ids.length + 1 > this.config.maxChunkBlocks || chunk.bytes + buffer.length > this.config.maxChunkBytes)) {
|
|
707
|
+
&& (chunk.ids.length + 1 > this.config.maxChunkBlocks || chunk.bytes + offer.buffer.length > this.config.maxChunkBytes)) {
|
|
595
708
|
await flush();
|
|
596
709
|
if (this.stopped || peerUnreachable())
|
|
597
710
|
break;
|
|
598
711
|
}
|
|
599
712
|
chunk.ids.push(blockId);
|
|
600
|
-
chunk.buffers.push(buffer);
|
|
601
|
-
chunk.meta[blockId] = { rev: latest.rev, actionId: latest.actionId };
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
// the block still ships with its meta and the receiver decides.
|
|
605
|
-
const proof = await storage.getBlockProof(blockId, latest.rev);
|
|
606
|
-
if (proof) {
|
|
607
|
-
chunk.proofs[blockId] = proof;
|
|
713
|
+
chunk.buffers.push(offer.buffer);
|
|
714
|
+
chunk.meta[blockId] = { rev: offer.latest.rev, actionId: offer.latest.actionId };
|
|
715
|
+
if (offer.proof) {
|
|
716
|
+
chunk.proofs[blockId] = offer.proof;
|
|
608
717
|
}
|
|
609
|
-
chunk.bytes += buffer.length;
|
|
718
|
+
chunk.bytes += offer.buffer.length;
|
|
610
719
|
result.offered += 1;
|
|
611
720
|
}
|
|
612
721
|
if (!this.stopped && !peerUnreachable()) {
|
|
613
722
|
await flush();
|
|
614
723
|
}
|
|
615
724
|
if (result.capped > 0) {
|
|
616
|
-
log('[%s] peer=%s catch-up CAPPED at %d blocks; %d block id(s) not attempted', this.deps.label,
|
|
725
|
+
log('[%s] peer=%s catch-up CAPPED at %d blocks; %d block id(s) not attempted', this.deps.label, key, this.config.maxBlocks, result.capped);
|
|
726
|
+
}
|
|
727
|
+
return { result, clean: !chunkFailed, pushFailed: chunkFailed };
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Read one block for the push to `key`, or count why it is skipped in `result` and return
|
|
731
|
+
* nothing: not committed, already held by the peer (memo or its own commit signature), not
|
|
732
|
+
* materialized, or too large for one message. The proof is read for EXACTLY the revision the
|
|
733
|
+
* meta will name, from the same `latest`; absent is normal (nothing retained a proof for that
|
|
734
|
+
* revision), and the block still ships with its meta for the receiver to decide.
|
|
735
|
+
*/
|
|
736
|
+
async readOffer(blockId, key, memo, result) {
|
|
737
|
+
const { storage } = this.deps;
|
|
738
|
+
const latest = (await storage.getMetadata(blockId))?.latest;
|
|
739
|
+
if (!latest) {
|
|
740
|
+
result.uncommitted += 1;
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
if (memoSettled(memo, blockId, latest.rev)) {
|
|
744
|
+
result.alreadyHeld += 1;
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
const proof = await storage.getBlockProof(blockId, latest.rev);
|
|
748
|
+
if (signedBy(proof, key)) {
|
|
749
|
+
result.alreadyHeld += 1;
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
// NOTE: a block whose latest revision is a DELETE materializes to nothing (a
|
|
753
|
+
// tombstone), so it is skipped here — a peer that joins after such a delete never
|
|
754
|
+
// receives the tombstone from this path and relies on read repair for it. Fine
|
|
755
|
+
// while nothing deletes whole blocks pre-join; if the whole-store coverage gate in
|
|
756
|
+
// strand-membership-closed-strand-e2e ever reports a tombstone residue, teach this
|
|
757
|
+
// to push the promoted delete transform instead of skipping.
|
|
758
|
+
const block = await storage.getMaterializedBlock(blockId, latest.actionId);
|
|
759
|
+
if (!block) {
|
|
760
|
+
result.unmaterialized += 1;
|
|
761
|
+
log('[%s] block=%s has latest rev %d but no materialized content; skipped', this.deps.label, blockId, latest.rev);
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
const buffer = blockEncoder.encode(JSON.stringify(block));
|
|
765
|
+
// NOTE: MAX_BLOCK_MESSAGE_BYTES caps the whole framed request, not one block, and
|
|
766
|
+
// this test ignores the request envelope (ids array, blockMeta, JSON punctuation).
|
|
767
|
+
// A lone block within a few KiB of the cap therefore still ships and is rejected by
|
|
768
|
+
// the receiver's length-prefix decoder — a permanent chunk failure for that peer. No
|
|
769
|
+
// block comes close today; if one ever can, subtract a measured envelope allowance
|
|
770
|
+
// here rather than raising the cap.
|
|
771
|
+
if (base64WireBytes(buffer.length) >= MAX_BLOCK_MESSAGE_BYTES) {
|
|
772
|
+
result.oversized.push(blockId);
|
|
773
|
+
log('[%s] block=%s is %d bytes (%d on the wire) — exceeds the %d-byte protocol cap alone; skipped', this.deps.label, blockId, buffer.length, base64WireBytes(buffer.length), MAX_BLOCK_MESSAGE_BYTES);
|
|
774
|
+
return undefined;
|
|
617
775
|
}
|
|
618
|
-
|
|
619
|
-
return { result, clean, pushFailed: chunkFailed };
|
|
776
|
+
return { latest, buffer, proof };
|
|
620
777
|
}
|
|
621
778
|
}
|
|
622
779
|
//# sourceMappingURL=peer-join-backfill.js.map
|