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