@lodestar/beacon-node 1.43.0-dev.87cbe69c66 → 1.43.0-dev.899087b3b2
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/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
- package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
- package/lib/chain/blocks/blockInput/blockInput.js +4 -1
- package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +0 -16
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
- package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +12 -1
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/blocks/index.d.ts.map +1 -1
- package/lib/chain/blocks/index.js +6 -7
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
- package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
- package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
- package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +25 -10
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/initState.d.ts.map +1 -1
- package/lib/chain/initState.js +6 -1
- package/lib/chain/initState.js.map +1 -1
- package/lib/chain/prepareNextSlot.js +1 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.d.ts +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
- package/lib/chain/seenCache/seenProposerPreferences.js +6 -11
- package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +14 -0
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/constants.d.ts +3 -1
- package/lib/sync/constants.d.ts.map +1 -1
- package/lib/sync/constants.js +3 -4
- package/lib/sync/constants.js.map +1 -1
- package/lib/sync/range/batch.d.ts +12 -3
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +163 -33
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts +13 -2
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +38 -4
- package/lib/sync/range/chain.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +14 -3
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/sync.d.ts.map +1 -1
- package/lib/sync/sync.js +13 -0
- package/lib/sync/sync.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts +7 -0
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +138 -10
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +29 -8
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +87 -42
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/package.json +16 -16
- package/src/chain/blocks/blockInput/blockInput.ts +4 -1
- package/src/chain/blocks/importBlock.ts +0 -18
- package/src/chain/blocks/importExecutionPayload.ts +18 -6
- package/src/chain/blocks/index.ts +5 -7
- package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
- package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
- package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
- package/src/chain/chain.ts +29 -9
- package/src/chain/initState.ts +9 -1
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
- package/src/chain/seenCache/seenProposerPreferences.ts +7 -12
- package/src/network/processor/gossipHandlers.ts +18 -0
- package/src/sync/constants.ts +4 -4
- package/src/sync/range/batch.ts +187 -38
- package/src/sync/range/chain.ts +52 -5
- package/src/sync/range/range.ts +16 -3
- package/src/sync/sync.ts +13 -1
- package/src/sync/unknownBlock.ts +170 -10
- package/src/sync/utils/downloadByRange.ts +148 -44
|
@@ -5,16 +5,16 @@ import {MapDef} from "@lodestar/utils";
|
|
|
5
5
|
* Tracks signed proposer preferences we've already seen per (dependent_root, proposal_slot, validator_index).
|
|
6
6
|
*/
|
|
7
7
|
export class SeenProposerPreferences {
|
|
8
|
-
private readonly
|
|
9
|
-
() => new Map<
|
|
8
|
+
private readonly validatorByDependentRootBySlot = new MapDef<Slot, Map<RootHex, ValidatorIndex>>(
|
|
9
|
+
() => new Map<RootHex, ValidatorIndex>()
|
|
10
10
|
);
|
|
11
11
|
|
|
12
12
|
isKnown(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
|
|
13
|
-
return this.
|
|
13
|
+
return this.validatorByDependentRootBySlot.get(proposalSlot)?.get(dependentRoot) === validatorIndex;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
add(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
|
|
17
|
-
this.
|
|
17
|
+
this.validatorByDependentRootBySlot.getOrDefault(proposalSlot).set(dependentRoot, validatorIndex);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -23,14 +23,9 @@ export class SeenProposerPreferences {
|
|
|
23
23
|
* on each slot tick.
|
|
24
24
|
*/
|
|
25
25
|
prune(currentSlot: Slot): void {
|
|
26
|
-
for (const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
slotMap.delete(slot);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (slotMap.size === 0) {
|
|
33
|
-
this.validatorBySlotByDependentRoot.delete(dependentRoot);
|
|
26
|
+
for (const slot of this.validatorByDependentRootBySlot.keys()) {
|
|
27
|
+
if (slot < currentSlot) {
|
|
28
|
+
this.validatorByDependentRootBySlot.delete(slot);
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
}
|
|
@@ -607,6 +607,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
607
607
|
// Returns the delay between the start of `block.slot` and `current time`
|
|
608
608
|
const delaySec = chain.clock.secFromSlot(slot);
|
|
609
609
|
metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
|
|
610
|
+
|
|
611
|
+
if (isForkPostGloas(blockInput.forkName)) {
|
|
612
|
+
const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
|
|
613
|
+
// This payloadInput should have been created just after gossip validation
|
|
614
|
+
if (!payloadInput) {
|
|
615
|
+
throw Error(
|
|
616
|
+
`PayloadEnvelopeInput not seeded for block ${blockInput.blockRootHex} during gossip processing`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments
|
|
621
|
+
// which is all the information we need so there is no reason to delay until execution payload arrives
|
|
622
|
+
// TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here
|
|
623
|
+
// (for example later in the slot). Do not couple retries to incoming gossip columns.
|
|
624
|
+
// Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any
|
|
625
|
+
// in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path.
|
|
626
|
+
chain.getBlobsTracker.triggerGetBlobs(payloadInput);
|
|
627
|
+
}
|
|
610
628
|
})
|
|
611
629
|
.catch((e) => {
|
|
612
630
|
// Adjust verbosity based on error type
|
package/src/sync/constants.ts
CHANGED
|
@@ -5,10 +5,10 @@ export const PARALLEL_HEAD_CHAINS = 2;
|
|
|
5
5
|
export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
|
|
6
6
|
|
|
7
7
|
/** The number of times to retry a batch before it is considered failed. */
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export const
|
|
8
|
+
export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
|
|
9
|
+
|
|
10
|
+
/** Backoff before assigning more range-sync batches to a peer that rate-limited us. */
|
|
11
|
+
export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Consider batch faulty after downloading and processing this number of times
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
2
|
import {ForkName, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
|
|
3
|
-
import {Epoch, RootHex, Slot, phase0} from "@lodestar/types";
|
|
4
|
-
import {LodestarError, prettyPrintIndices} from "@lodestar/utils";
|
|
3
|
+
import {Epoch, RootHex, SignedBeaconBlock, Slot, gloas, phase0} from "@lodestar/types";
|
|
4
|
+
import {LodestarError, byteArrayEquals, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
5
5
|
import {isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
|
|
6
6
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
7
7
|
import {isDaOutOfRange} from "../../chain/blocks/blockInput/utils.js";
|
|
8
8
|
import {PayloadEnvelopeInput} from "../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
|
|
9
9
|
import {BlockError, BlockErrorCode} from "../../chain/errors/index.js";
|
|
10
|
+
import {ZERO_HASH} from "../../constants/constants.js";
|
|
10
11
|
import {PeerSyncMeta} from "../../network/peers/peersData.js";
|
|
11
12
|
import {IClock} from "../../util/clock.js";
|
|
12
13
|
import {CustodyConfig} from "../../util/dataColumns.js";
|
|
13
14
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
14
15
|
import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
|
|
15
|
-
import {DownloadByRangeRequests} from "../utils/downloadByRange.js";
|
|
16
|
+
import {DownloadByRangeRequests, ParentPayloadCommitments} from "../utils/downloadByRange.js";
|
|
16
17
|
import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -141,8 +142,18 @@ export class Batch {
|
|
|
141
142
|
private readonly config: ChainForkConfig;
|
|
142
143
|
private readonly clock: IClock;
|
|
143
144
|
private readonly custodyConfig: CustodyConfig;
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
private readonly isFirstBatchInChain: boolean;
|
|
146
|
+
private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
|
|
147
|
+
|
|
148
|
+
constructor(
|
|
149
|
+
startEpoch: Epoch,
|
|
150
|
+
config: ChainForkConfig,
|
|
151
|
+
clock: IClock,
|
|
152
|
+
custodyConfig: CustodyConfig,
|
|
153
|
+
isFirstBatchInChain: boolean,
|
|
154
|
+
latestBid: gloas.ExecutionPayloadBid | undefined,
|
|
155
|
+
targetSlot: Slot
|
|
156
|
+
) {
|
|
146
157
|
this.config = config;
|
|
147
158
|
this.clock = clock;
|
|
148
159
|
this.custodyConfig = custodyConfig;
|
|
@@ -151,10 +162,40 @@ export class Batch {
|
|
|
151
162
|
this.forkName = this.config.getForkName(startSlot);
|
|
152
163
|
this.startEpoch = startEpoch;
|
|
153
164
|
this.startSlot = startSlot;
|
|
154
|
-
this.count = count;
|
|
165
|
+
this.count = Math.min(count, targetSlot - startSlot + 1);
|
|
166
|
+
this.isFirstBatchInChain = isFirstBatchInChain;
|
|
167
|
+
this.latestBid = latestBid;
|
|
155
168
|
this.requests = this.getRequests([]);
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
|
|
172
|
+
if (!this.isFirstBatchInChain) return false;
|
|
173
|
+
|
|
174
|
+
if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// we only know if we should download parent envelope if firstBlock is downloaded
|
|
179
|
+
if (firstBlock === undefined) return false;
|
|
180
|
+
if (this.latestBid === undefined) return false;
|
|
181
|
+
const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
|
|
182
|
+
.parentBlockHash;
|
|
183
|
+
return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
|
|
187
|
+
if (this.latestBid === undefined) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
blockRoot: parentBlockRoot,
|
|
194
|
+
blockRootHex: toRootHex(parentBlockRoot),
|
|
195
|
+
kzgCommitments: this.latestBid.blobKzgCommitments,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
/**
|
|
159
200
|
* Builds ByRange requests for block, blobs and columns
|
|
160
201
|
*/
|
|
@@ -202,6 +243,7 @@ export class Batch {
|
|
|
202
243
|
const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
|
|
203
244
|
|
|
204
245
|
// ensure blocks are in slot-wise order
|
|
246
|
+
const isPostGloas = isForkPostGloas(this.forkName);
|
|
205
247
|
for (const blockInput of blocks) {
|
|
206
248
|
const blockSlot = blockInput.slot;
|
|
207
249
|
// check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
|
|
@@ -217,21 +259,36 @@ export class Batch {
|
|
|
217
259
|
if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
|
|
218
260
|
blockStartSlot = blockSlot + 1;
|
|
219
261
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
262
|
+
|
|
263
|
+
// Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
|
|
264
|
+
// which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
|
|
265
|
+
// a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
|
|
266
|
+
if (isPostGloas) {
|
|
267
|
+
// Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
|
|
268
|
+
const payloadInput = envelopesBySlot.get(blockSlot);
|
|
269
|
+
if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
|
|
270
|
+
envelopeStartSlot = blockSlot + 1;
|
|
271
|
+
}
|
|
272
|
+
if (payloadInput && !payloadInput.hasComputedAllData()) {
|
|
273
|
+
for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
|
|
230
274
|
neededColumns.add(index);
|
|
231
275
|
}
|
|
276
|
+
} else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
|
|
277
|
+
// Only advance dataStartSlot when we know columns for this slot are complete. If
|
|
278
|
+
// payloadInput is missing entirely we cannot tell, so stop here so the next round
|
|
279
|
+
// re-requests columns (and envelopes) starting at this slot.
|
|
280
|
+
dataStartSlot = blockSlot + 1;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
|
|
284
|
+
if (isBlockInputColumns(blockInput)) {
|
|
285
|
+
for (const index of blockInput.getMissingSampledColumnMeta().missing) {
|
|
286
|
+
neededColumns.add(index);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else if (dataStartSlot === blockSlot) {
|
|
290
|
+
dataStartSlot = blockSlot + 1;
|
|
232
291
|
}
|
|
233
|
-
} else if (dataStartSlot === blockSlot) {
|
|
234
|
-
dataStartSlot = blockSlot + 1;
|
|
235
292
|
}
|
|
236
293
|
}
|
|
237
294
|
|
|
@@ -251,11 +308,15 @@ export class Batch {
|
|
|
251
308
|
// range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
|
|
252
309
|
const count = endSlot - dataStartSlot + 1;
|
|
253
310
|
if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
311
|
+
// Skip the column re-request when we have no specific column indices outstanding.
|
|
312
|
+
// Peer rejects an empty `columns` list
|
|
313
|
+
if (neededColumns.size > 0) {
|
|
314
|
+
requests.columnsRequest = {
|
|
315
|
+
count,
|
|
316
|
+
startSlot: dataStartSlot,
|
|
317
|
+
columns: Array.from(neededColumns),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
259
320
|
} else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
|
|
260
321
|
requests.blobsRequest = {
|
|
261
322
|
count,
|
|
@@ -272,6 +333,36 @@ export class Batch {
|
|
|
272
333
|
};
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
// Only the first batch of a SyncChain may need the dangling-parent payload by-root.
|
|
337
|
+
if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
|
|
338
|
+
// shouldDownloadParentEnvelope() = true means there are at least 1 block
|
|
339
|
+
const parentRoot = blocks[0].getBlock().message.parentRoot;
|
|
340
|
+
if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
|
|
341
|
+
const parentRootHex = toRootHex(parentRoot);
|
|
342
|
+
let parentPayloadInput: PayloadEnvelopeInput | undefined;
|
|
343
|
+
if (this.state.payloadEnvelopes) {
|
|
344
|
+
for (const pi of this.state.payloadEnvelopes.values()) {
|
|
345
|
+
if (pi.blockRootHex === parentRootHex) {
|
|
346
|
+
parentPayloadInput = pi;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
|
|
353
|
+
const missingColumns = parentPayloadInput
|
|
354
|
+
? parentPayloadInput.getMissingSampledColumnMeta().missing
|
|
355
|
+
: this.custodyConfig.sampledColumns;
|
|
356
|
+
|
|
357
|
+
if (needsEnvelope || missingColumns.length > 0) {
|
|
358
|
+
requests.parentPayloadRequest = {
|
|
359
|
+
...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
|
|
360
|
+
...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
275
366
|
return requests;
|
|
276
367
|
}
|
|
277
368
|
|
|
@@ -283,24 +374,28 @@ export class Batch {
|
|
|
283
374
|
return this.requests;
|
|
284
375
|
}
|
|
285
376
|
|
|
286
|
-
// post-fulu we need to ensure that we only request columns that the peer has advertised
|
|
287
|
-
const {columnsRequest} = this.requests;
|
|
288
|
-
if (columnsRequest == null) {
|
|
289
|
-
return this.requests;
|
|
290
|
-
}
|
|
377
|
+
// post-fulu we need to ensure that we only request columns that the peer has advertised.
|
|
378
|
+
const {columnsRequest, parentPayloadRequest} = this.requests;
|
|
291
379
|
|
|
292
380
|
const peerColumns = new Set(peer.custodyColumns ?? []);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
381
|
+
const filteredColumnsRequest =
|
|
382
|
+
columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
|
|
383
|
+
const parentColumns = parentPayloadRequest?.columns;
|
|
384
|
+
const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
|
|
385
|
+
|
|
386
|
+
const updatedColumnRequest =
|
|
387
|
+
columnsRequest != null && filteredColumnsRequest != null
|
|
388
|
+
? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
|
|
389
|
+
: {};
|
|
390
|
+
const updatedParentPayloadRequest =
|
|
391
|
+
parentPayloadRequest != null && filteredParentColumns != null
|
|
392
|
+
? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
|
|
393
|
+
: {};
|
|
297
394
|
|
|
298
395
|
return {
|
|
299
396
|
...this.requests,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
columns: requestedColumns,
|
|
303
|
-
},
|
|
397
|
+
...updatedColumnRequest,
|
|
398
|
+
...updatedParentPayloadRequest,
|
|
304
399
|
};
|
|
305
400
|
}
|
|
306
401
|
|
|
@@ -379,7 +474,11 @@ export class Batch {
|
|
|
379
474
|
const slots = new Set<number>();
|
|
380
475
|
for (const block of blocks) {
|
|
381
476
|
slots.add(block.slot);
|
|
382
|
-
|
|
477
|
+
const dataComplete = isBlockInputColumns(block)
|
|
478
|
+
? // by_range needs to download all columns
|
|
479
|
+
block.hasBlock() && block.hasComputedAllData()
|
|
480
|
+
: block.hasBlockAndAllData();
|
|
481
|
+
if (!dataComplete) {
|
|
383
482
|
allComplete = false;
|
|
384
483
|
}
|
|
385
484
|
}
|
|
@@ -395,11 +494,45 @@ export class Batch {
|
|
|
395
494
|
}
|
|
396
495
|
const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
|
|
397
496
|
|
|
497
|
+
if (allComplete && isForkPostGloas(this.forkName)) {
|
|
498
|
+
for (const block of blocks) {
|
|
499
|
+
const payloadInput = newPayloadEnvelopes?.get(block.slot);
|
|
500
|
+
// by_range needs every block's envelope and all sampled columns.
|
|
501
|
+
if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
|
|
502
|
+
allComplete = false;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// First batch of a sync chain must additionally have the dangling-parent payload fully
|
|
509
|
+
// present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
|
|
510
|
+
// `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
|
|
511
|
+
if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
|
|
512
|
+
const parentRoot = blocks[0].getBlock().message.parentRoot;
|
|
513
|
+
// Genesis has no parent payload — nothing to wait for.
|
|
514
|
+
if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
|
|
515
|
+
const parentRootHex = toRootHex(parentRoot);
|
|
516
|
+
let parentPayloadComplete = false;
|
|
517
|
+
if (newPayloadEnvelopes) {
|
|
518
|
+
for (const payloadInput of newPayloadEnvelopes.values()) {
|
|
519
|
+
if (payloadInput.blockRootHex === parentRootHex) {
|
|
520
|
+
parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!parentPayloadComplete) {
|
|
526
|
+
allComplete = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
398
531
|
if (allComplete) {
|
|
399
532
|
this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
|
|
400
533
|
} else {
|
|
401
|
-
this.requests = this.getRequests(blocks);
|
|
402
534
|
this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
|
|
535
|
+
this.requests = this.getRequests(blocks);
|
|
403
536
|
}
|
|
404
537
|
|
|
405
538
|
return this.state as DownloadSuccessState;
|
|
@@ -425,6 +558,22 @@ export class Batch {
|
|
|
425
558
|
};
|
|
426
559
|
}
|
|
427
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Downloading -> AwaitingDownload (without counting as a failed attempt).
|
|
563
|
+
* Used when the peer rate-limited us — the request was never actually served.
|
|
564
|
+
*/
|
|
565
|
+
downloadingRateLimited(): void {
|
|
566
|
+
if (this.state.status !== BatchStatus.Downloading) {
|
|
567
|
+
throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.state = {
|
|
571
|
+
status: BatchStatus.AwaitingDownload,
|
|
572
|
+
blocks: this.state.blocks,
|
|
573
|
+
payloadEnvelopes: this.state.payloadEnvelopes,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
428
577
|
/**
|
|
429
578
|
* AwaitingProcessing -> Processing
|
|
430
579
|
*/
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {
|
|
2
|
+
import {RequestErrorCode} from "@lodestar/reqresp";
|
|
3
|
+
import {Epoch, Root, Slot, gloas} from "@lodestar/types";
|
|
3
4
|
import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
4
5
|
import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
|
|
5
6
|
import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
|
|
@@ -15,7 +16,12 @@ import {CustodyConfig} from "../../util/dataColumns.js";
|
|
|
15
16
|
import {ItTrigger} from "../../util/itTrigger.js";
|
|
16
17
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
17
18
|
import {WarnResult, wrapError} from "../../util/wrapError.js";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
BATCH_BUFFER_SIZE,
|
|
21
|
+
EPOCHS_PER_BATCH,
|
|
22
|
+
MAX_LOOK_AHEAD_EPOCHS,
|
|
23
|
+
RATE_LIMITED_PEER_BACKOFF_MS,
|
|
24
|
+
} from "../constants.js";
|
|
19
25
|
import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
|
|
20
26
|
import {RangeSyncType} from "../utils/remoteSyncType.js";
|
|
21
27
|
import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
|
|
@@ -139,20 +145,32 @@ export class SyncChain {
|
|
|
139
145
|
private readonly batchProcessor = new ItTrigger();
|
|
140
146
|
/** Sorted map of batches undergoing some kind of processing. */
|
|
141
147
|
private readonly batches = new Map<Epoch, Batch>();
|
|
148
|
+
/**
|
|
149
|
+
* `true` until the first `Batch` is constructed via `includeNextBatch`
|
|
150
|
+
*/
|
|
151
|
+
private isFirstBatch = true;
|
|
142
152
|
private readonly peerset = new Map<PeerIdStr, ChainTarget>();
|
|
153
|
+
/**
|
|
154
|
+
* Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
|
|
155
|
+
* This is a sync-layer optimization to avoid assigning batches to backed-off peers.
|
|
156
|
+
* The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
|
|
157
|
+
*/
|
|
158
|
+
private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
|
|
143
159
|
|
|
144
160
|
private readonly logger: Logger;
|
|
145
161
|
private readonly config: ChainForkConfig;
|
|
146
162
|
private readonly clock: IClock;
|
|
147
163
|
private readonly metrics: Metrics | null;
|
|
148
164
|
private readonly custodyConfig: CustodyConfig;
|
|
165
|
+
private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
|
|
149
166
|
|
|
150
167
|
constructor(
|
|
151
168
|
initialBatchEpoch: Epoch,
|
|
152
169
|
initialTarget: ChainTarget,
|
|
153
170
|
syncType: RangeSyncType,
|
|
154
171
|
fns: SyncChainFns,
|
|
155
|
-
modules: SyncChainModules
|
|
172
|
+
modules: SyncChainModules,
|
|
173
|
+
latestBid: gloas.ExecutionPayloadBid | undefined
|
|
156
174
|
) {
|
|
157
175
|
const {config, clock, custodyConfig, logger, metrics} = modules;
|
|
158
176
|
this.firstBatchEpoch = initialBatchEpoch;
|
|
@@ -168,6 +186,7 @@ export class SyncChain {
|
|
|
168
186
|
this.clock = clock;
|
|
169
187
|
this.metrics = metrics;
|
|
170
188
|
this.custodyConfig = custodyConfig;
|
|
189
|
+
this.latestBid = latestBid;
|
|
171
190
|
this.logger = logger;
|
|
172
191
|
this.logId = `${syncType}-${nextChainId++}`;
|
|
173
192
|
|
|
@@ -248,6 +267,7 @@ export class SyncChain {
|
|
|
248
267
|
*/
|
|
249
268
|
removePeer(peerId: PeerIdStr): boolean {
|
|
250
269
|
const deleted = this.peerset.delete(peerId);
|
|
270
|
+
this.rateLimitedPeers.delete(peerId);
|
|
251
271
|
this.computeTarget();
|
|
252
272
|
return deleted;
|
|
253
273
|
}
|
|
@@ -383,8 +403,18 @@ export class SyncChain {
|
|
|
383
403
|
return;
|
|
384
404
|
}
|
|
385
405
|
|
|
406
|
+
const now = Date.now();
|
|
386
407
|
const peersSyncInfo: PeerSyncInfo[] = [];
|
|
387
408
|
for (const [peerId, target] of this.peerset.entries()) {
|
|
409
|
+
// Skip peers that are currently in rate-limit backoff
|
|
410
|
+
const rateLimitedUntil = this.rateLimitedPeers.get(peerId);
|
|
411
|
+
if (rateLimitedUntil !== undefined) {
|
|
412
|
+
if (now < rateLimitedUntil) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
this.rateLimitedPeers.delete(peerId);
|
|
416
|
+
}
|
|
417
|
+
|
|
388
418
|
try {
|
|
389
419
|
peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
|
|
390
420
|
} catch (e) {
|
|
@@ -458,7 +488,17 @@ export class SyncChain {
|
|
|
458
488
|
return null;
|
|
459
489
|
}
|
|
460
490
|
|
|
461
|
-
const batch = new Batch(
|
|
491
|
+
const batch = new Batch(
|
|
492
|
+
startEpoch,
|
|
493
|
+
this.config,
|
|
494
|
+
this.clock,
|
|
495
|
+
this.custodyConfig,
|
|
496
|
+
this.isFirstBatch,
|
|
497
|
+
// `latestBid` is only meaningful for the first batch's parent-payload check
|
|
498
|
+
this.isFirstBatch ? this.latestBid : undefined,
|
|
499
|
+
this.target.slot
|
|
500
|
+
);
|
|
501
|
+
this.isFirstBatch = false;
|
|
462
502
|
this.batches.set(startEpoch, batch);
|
|
463
503
|
return batch;
|
|
464
504
|
}
|
|
@@ -516,7 +556,14 @@ export class SyncChain {
|
|
|
516
556
|
{id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
|
|
517
557
|
res.err
|
|
518
558
|
);
|
|
519
|
-
|
|
559
|
+
if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
|
|
560
|
+
// Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
|
|
561
|
+
this.rateLimitedPeers.set(peer.peerId, Date.now() + RATE_LIMITED_PEER_BACKOFF_MS);
|
|
562
|
+
batch.downloadingRateLimited();
|
|
563
|
+
this.triggerBatchDownloader();
|
|
564
|
+
} else {
|
|
565
|
+
batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
|
|
566
|
+
}
|
|
520
567
|
} else {
|
|
521
568
|
this.logger.verbose("Batch download success", {
|
|
522
569
|
id: this.logId,
|
package/src/sync/range/range.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EventEmitter} from "node:events";
|
|
2
2
|
import {StrictEventEmitter} from "strict-event-emitter-types";
|
|
3
3
|
import {BeaconConfig} from "@lodestar/config";
|
|
4
|
-
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
4
|
+
import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
|
|
5
5
|
import {Epoch, Status, fulu} from "@lodestar/types";
|
|
6
6
|
import {Logger, toRootHex} from "@lodestar/utils";
|
|
7
7
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
@@ -206,14 +206,18 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
206
206
|
|
|
207
207
|
private downloadByRange: SyncChainFns["downloadByRange"] = async (peer, batch) => {
|
|
208
208
|
const batchBlocks = batch.getBlocks();
|
|
209
|
+
const requests = batch.getRequestsForPeer(peer);
|
|
210
|
+
const parentRoot = requests.parentPayloadRequest?.envelopeBlockRoot ?? requests.parentPayloadRequest?.blockRoot;
|
|
211
|
+
const parentPayloadCommitments = parentRoot ? batch.getParentPayloadCommitments(parentRoot) : undefined;
|
|
209
212
|
const {result, warnings} = await downloadByRange({
|
|
210
213
|
config: this.config,
|
|
211
214
|
network: this.network,
|
|
212
215
|
logger: this.logger,
|
|
213
216
|
peerIdStr: peer.peerId,
|
|
214
217
|
batchBlocks,
|
|
218
|
+
parentPayloadCommitments,
|
|
215
219
|
peerDasMetrics: this.chain.metrics?.peerDas,
|
|
216
|
-
...
|
|
220
|
+
...requests,
|
|
217
221
|
});
|
|
218
222
|
const {responses, payloadEnvelopes: downloadedPayloadEnvelopes} = result;
|
|
219
223
|
const {blocks, payloadEnvelopes} = cacheByRangeResponses({
|
|
@@ -258,6 +262,14 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
258
262
|
private addPeerOrCreateChain(startEpoch: Epoch, target: ChainTarget, peer: PeerIdStr, syncType: RangeSyncType): void {
|
|
259
263
|
let syncChain = this.chains.get(syncType);
|
|
260
264
|
if (!syncChain) {
|
|
265
|
+
// The first batch of a new sync chain may need to detect whether the parent block was an
|
|
266
|
+
// gloas "empty" block (no envelope produced). It does so by comparing the first
|
|
267
|
+
// downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`.
|
|
268
|
+
const headState = this.chain.getHeadState();
|
|
269
|
+
const latestBid = isStatePostGloas(headState)
|
|
270
|
+
? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid
|
|
271
|
+
: undefined;
|
|
272
|
+
|
|
261
273
|
syncChain = new SyncChain(
|
|
262
274
|
startEpoch,
|
|
263
275
|
target,
|
|
@@ -276,7 +288,8 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
276
288
|
logger: this.logger,
|
|
277
289
|
custodyConfig: this.chain.custodyConfig,
|
|
278
290
|
metrics: this.metrics,
|
|
279
|
-
}
|
|
291
|
+
},
|
|
292
|
+
latestBid
|
|
280
293
|
);
|
|
281
294
|
this.chains.set(syncType, syncChain);
|
|
282
295
|
|
package/src/sync/sync.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
2
2
|
import {Slot} from "@lodestar/types";
|
|
3
|
-
import {Logger} from "@lodestar/utils";
|
|
3
|
+
import {Logger, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {IBeaconChain} from "../chain/index.js";
|
|
5
5
|
import {GENESIS_SLOT} from "../constants/constants.js";
|
|
6
6
|
import {ExecutionEngineState} from "../execution/index.js";
|
|
@@ -188,6 +188,18 @@ export class BeaconSync implements IBeaconSync {
|
|
|
188
188
|
private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
|
|
189
189
|
const localStatus = this.chain.getStatus();
|
|
190
190
|
const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance);
|
|
191
|
+
this.logger.verbose("Peer sync type classified", {
|
|
192
|
+
peer: data.peer,
|
|
193
|
+
syncType,
|
|
194
|
+
localFinalizedEpoch: localStatus.finalizedEpoch,
|
|
195
|
+
localFinalizedRoot: toRootHex(localStatus.finalizedRoot),
|
|
196
|
+
localHeadSlot: localStatus.headSlot,
|
|
197
|
+
localHeadRoot: toRootHex(localStatus.headRoot),
|
|
198
|
+
remoteFinalizedEpoch: data.status.finalizedEpoch,
|
|
199
|
+
remoteFinalizedRoot: toRootHex(data.status.finalizedRoot),
|
|
200
|
+
remoteHeadSlot: data.status.headSlot,
|
|
201
|
+
remoteHeadRoot: toRootHex(data.status.headRoot),
|
|
202
|
+
});
|
|
191
203
|
|
|
192
204
|
// For metrics only
|
|
193
205
|
this.peerSyncType.set(data.peer, syncType);
|