@lodestar/beacon-node 1.43.0-dev.be5f70c0c8 → 1.43.0-dev.c9dc2b2332

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.
@@ -45,6 +45,15 @@ export type Attempt = {
45
45
  hash: RootHex;
46
46
  };
47
47
 
48
+ type TrackedRequest = {
49
+ /** only happen for the 1st batch in checkpoint sync */
50
+ parentPayload: boolean;
51
+ /**
52
+ * we always issue by_range before parent_payload, so we don't model this as null
53
+ */
54
+ byRangeColumns: Set<number>;
55
+ };
56
+
48
57
  export type AwaitingDownloadState = {
49
58
  status: BatchStatus.AwaitingDownload;
50
59
  blocks: IBlockInput[];
@@ -62,6 +71,7 @@ export type BatchState =
62
71
  | {
63
72
  status: BatchStatus.Downloading;
64
73
  peer: PeerIdStr;
74
+ request: TrackedRequest;
65
75
  blocks: IBlockInput[];
66
76
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
67
77
  }
@@ -110,6 +120,13 @@ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[
110
120
  return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
111
121
  }
112
122
 
123
+ function getTrackedRequest({parentPayloadRequest, columnsRequest}: DownloadByRangeRequests): TrackedRequest {
124
+ return {
125
+ parentPayload: parentPayloadRequest != null,
126
+ byRangeColumns: new Set(parentPayloadRequest == null ? (columnsRequest?.columns ?? []) : []),
127
+ };
128
+ }
129
+
113
130
  /**
114
131
  * Batches are downloaded at the first block of the epoch.
115
132
  *
@@ -131,8 +148,8 @@ export class Batch {
131
148
  requests: DownloadByRangeRequests;
132
149
  /** State of the batch. */
133
150
  state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
134
- /** Peers that provided good data */
135
- goodPeers: PeerIdStr[] = [];
151
+ /** Peers that provided good data, with column coverage for by_range requests */
152
+ private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
136
153
  /** The `Attempts` that have been made and failed to send us this batch. */
137
154
  readonly failedProcessingAttempts: Attempt[] = [];
138
155
  /** The `Attempts` that have been made and failed because of execution malfunction. */
@@ -406,6 +423,35 @@ export class Batch {
406
423
  return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
407
424
  }
408
425
 
426
+ /**
427
+ * True only if the peer has already returned a successful response for the current request.
428
+ * A by_range success may update `this.requests` to parent_payload, and the same peer is then
429
+ * still eligible for the newly discovered parent payload data.
430
+ * For by_range, a peer that previously succeeded with a superset of requested columns is skipped.
431
+ */
432
+ hasPeerSucceededCurrentRequest(peer: PeerSyncMeta): boolean {
433
+ const successfulDownload = this.successfulDownloads.get(peer.peerId);
434
+ if (successfulDownload == null) return false;
435
+
436
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
437
+ if (request.parentPayload) return successfulDownload.parentPayload;
438
+
439
+ const requestByRangeColumns = request.byRangeColumns;
440
+
441
+ if (requestByRangeColumns.size === 0) {
442
+ // this means a download blocks/envelops by_range only
443
+ // don't do that again if we already did it
444
+ // see https://github.com/ChainSafe/lodestar/issues/9357
445
+ return true;
446
+ }
447
+
448
+ return [...requestByRangeColumns].every((column) => successfulDownload.byRangeColumns.has(column));
449
+ }
450
+
451
+ private getSuccessfulPeers(): PeerIdStr[] {
452
+ return Array.from(this.successfulDownloads.keys());
453
+ }
454
+
409
455
  getMetadata(): BatchMetadata {
410
456
  const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
411
457
  const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
@@ -440,14 +486,17 @@ export class Batch {
440
486
  /**
441
487
  * AwaitingDownload -> Downloading
442
488
  */
443
- startDownloading(peer: PeerIdStr): void {
489
+ startDownloading(peer: PeerSyncMeta): void {
444
490
  if (this.state.status !== BatchStatus.AwaitingDownload) {
445
491
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingDownload));
446
492
  }
447
493
 
494
+ const request = getTrackedRequest(this.getRequestsForPeer(peer));
495
+
448
496
  this.state = {
449
497
  status: BatchStatus.Downloading,
450
- peer,
498
+ peer: peer.peerId,
499
+ request,
451
500
  blocks: this.state.blocks,
452
501
  payloadEnvelopes: this.state.payloadEnvelopes,
453
502
  };
@@ -468,7 +517,17 @@ export class Batch {
468
517
  // ensure that blocks are always sorted before getting stored on the batch.state or being used to getRequests
469
518
  blocks.sort((a, b) => a.slot - b.slot);
470
519
 
471
- this.goodPeers.push(peer);
520
+ const successfulDownload = this.successfulDownloads.get(peer) ?? {
521
+ parentPayload: false,
522
+ byRangeColumns: new Set<number>(),
523
+ };
524
+ successfulDownload.parentPayload ||= this.state.request.parentPayload;
525
+ if (!this.state.request.parentPayload) {
526
+ for (const column of this.state.request.byRangeColumns) {
527
+ successfulDownload.byRangeColumns.add(column);
528
+ }
529
+ }
530
+ this.successfulDownloads.set(peer, successfulDownload);
472
531
 
473
532
  let allComplete = true;
474
533
  const slots = new Set<number>();
@@ -497,8 +556,9 @@ export class Batch {
497
556
  if (allComplete && isForkPostGloas(this.forkName)) {
498
557
  for (const block of blocks) {
499
558
  const payloadInput = newPayloadEnvelopes?.get(block.slot);
500
- // by_range needs every block's envelope and all sampled columns.
501
- if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
559
+ // only need to make sure envelope has all columns, not all blocks have payload
560
+ // assertLinearChainSegment() was called before reaching this
561
+ if (payloadInput?.hasPayloadEnvelope() && !payloadInput.hasComputedAllData()) {
502
562
  allComplete = false;
503
563
  break;
504
564
  }
@@ -589,10 +649,10 @@ export class Batch {
589
649
  const blocks = this.state.blocks;
590
650
  const payloadEnvelopes = this.state.payloadEnvelopes;
591
651
  const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
592
- // Reset goodPeers in case another download attempt needs to be made. When Attempt is successful or not the peers
593
- // that the data came from will be handled by the Attempt that goes for processing
594
- const peers = this.goodPeers;
595
- this.goodPeers = [];
652
+ // Reset successfulDownloads in case another download attempt needs to be made. When Attempt is successful or not
653
+ // the peers that the data came from will be handled by the Attempt that goes for processing.
654
+ const peers = this.getSuccessfulPeers();
655
+ this.successfulDownloads.clear();
596
656
  this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
597
657
  return {blocks, payloadEnvelopes, peers};
598
658
  }
@@ -551,7 +551,7 @@ export class SyncChain {
551
551
  peer: prettyPrintPeerIdStr(peer.peerId),
552
552
  });
553
553
  try {
554
- batch.startDownloading(peer.peerId);
554
+ batch.startDownloading(peer);
555
555
 
556
556
  // wrapError ensures to never call both batch success() and batch error()
557
557
  const res = await wrapError(this.downloadByRange(peer, batch, this.syncType));
@@ -581,6 +581,8 @@ export class SyncChain {
581
581
  case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS:
582
582
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
583
583
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
584
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
585
+ case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
584
586
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
585
587
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
586
588
  case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT:
@@ -3,14 +3,21 @@ import {StrictEventEmitter} from "strict-event-emitter-types";
3
3
  import {BeaconConfig} from "@lodestar/config";
4
4
  import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
5
5
  import {Epoch, Status, fulu} from "@lodestar/types";
6
- import {Logger, toRootHex} from "@lodestar/utils";
6
+ import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
7
7
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
8
8
  import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
9
+ import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
10
+ import {BlockError} from "../../chain/errors/index.js";
9
11
  import {IBeaconChain} from "../../chain/index.js";
10
12
  import {Metrics} from "../../metrics/index.js";
11
13
  import {INetwork} from "../../network/index.js";
12
14
  import {PeerIdStr} from "../../util/peerId.js";
13
- import {cacheByRangeResponses, downloadByRange} from "../utils/downloadByRange.js";
15
+ import {
16
+ DownloadByRangeError,
17
+ DownloadByRangeErrorCode,
18
+ cacheByRangeResponses,
19
+ downloadByRange,
20
+ } from "../utils/downloadByRange.js";
14
21
  import {RangeSyncType, getRangeSyncTarget, rangeSyncTypes} from "../utils/remoteSyncType.js";
15
22
  import {ChainTarget, SyncChain, SyncChainDebugState, SyncChainFns} from "./chain.js";
16
23
  import {updateChains} from "./utils/index.js";
@@ -231,6 +238,49 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
231
238
  custodyConfig: this.chain.custodyConfig,
232
239
  seenTimestampSec: Date.now() / 1000,
233
240
  });
241
+
242
+ const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot);
243
+ const envelopeSlots = payloadEnvelopes
244
+ ? Array.from(payloadEnvelopes.entries())
245
+ .filter(([, pi]) => pi.hasPayloadEnvelope())
246
+ .map(([slot]) => slot)
247
+ .sort((a, b) => a - b)
248
+ : [];
249
+ this.logger.verbose("downloadByRange batch ready", {
250
+ peer: peer.peerId,
251
+ blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)),
252
+ envelopeSlots: prettyPrintIndices(envelopeSlots),
253
+ ...batch.getMetadata(),
254
+ });
255
+
256
+ if (segmentBlocks.length > 1) {
257
+ try {
258
+ assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null);
259
+ } catch (err) {
260
+ if (err instanceof BlockError) {
261
+ this.logger.debug(
262
+ "downloadByRange segment validation failed",
263
+ {
264
+ peer: peer.peerId,
265
+ reason: err.type.code,
266
+ slot: err.signedBlock.message.slot,
267
+ detail: JSON.stringify(err.type),
268
+ ...batch.getMetadata(),
269
+ },
270
+ err
271
+ );
272
+ // with this error, the peer will be penalized inside SyncChain
273
+ throw new DownloadByRangeError({
274
+ code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT,
275
+ slot: err.signedBlock.message.slot,
276
+ reason: err.type.code,
277
+ });
278
+ }
279
+
280
+ throw err;
281
+ }
282
+ }
283
+
234
284
  return {result: {blocks, payloadEnvelopes}, warnings};
235
285
  };
236
286
 
@@ -52,7 +52,8 @@ export class ChainPeersBalancer {
52
52
 
53
53
  /**
54
54
  * Return the most suitable peer to retry
55
- * Sort peers by (1) no failed request (2) less active requests, then pick first
55
+ * Sort peers by (1) less active requests (2) most columns we need.
56
+ * Peers that failed this batch or already succeeded for the same request are excluded inside `filterPeers`.
56
57
  */
57
58
  bestPeerToRetryBatch(batch: Batch): PeerSyncMeta | undefined {
58
59
  if (batch.state.status !== BatchStatus.AwaitingDownload) {
@@ -63,10 +64,8 @@ export class ChainPeersBalancer {
63
64
  const pendingDataColumns = columnsRequest?.columns ?? this.custodyConfig.sampledColumns;
64
65
  const eligiblePeers = this.filterPeers(batch, pendingDataColumns, false);
65
66
 
66
- const failedPeers = new Set(batch.getFailedPeers());
67
67
  const sortedBestPeers = sortBy(
68
68
  eligiblePeers,
69
- ({syncInfo}) => (failedPeers.has(syncInfo.peerId) ? 1 : 0), // prefer peers without failed requests
70
69
  ({syncInfo}) => this.activeRequestsByPeer.get(syncInfo.peerId) ?? 0, // prefer peers with least active req
71
70
  ({columns}) => -1 * columns // prefer peers with the most columns
72
71
  );
@@ -117,9 +116,16 @@ export class ChainPeersBalancer {
117
116
  return eligiblePeers;
118
117
  }
119
118
 
119
+ // Skip peers that failed this batch, or that already returned the exact current request shape.
120
+ const failedPeers = new Set<PeerIdStr>(batch.getFailedPeers());
121
+
120
122
  for (const peer of this.peers) {
121
123
  const {earliestAvailableSlot, target, peerId} = peer;
122
124
 
125
+ if (failedPeers.has(peerId) || batch.hasPeerSucceededCurrentRequest(peer)) {
126
+ continue;
127
+ }
128
+
123
129
  const activeRequest = this.activeRequestsByPeer.get(peerId) ?? 0;
124
130
  if (noActiveRequest && activeRequest > 0) {
125
131
  // consumer wants to find peer with no active request, but this peer has active request
@@ -1155,6 +1155,9 @@ export enum DownloadByRangeErrorCode {
1155
1155
 
1156
1156
  /** Envelope beaconBlockRoot does not match the block's root */
1157
1157
  INVALID_ENVELOPE_BEACON_BLOCK_ROOT = "DOWNLOAD_BY_RANGE_ERROR_INVALID_ENVELOPE_BEACON_BLOCK_ROOT",
1158
+
1159
+ /** Block segment + envelopes failed chain-segment linearity / FULL-chain checks */
1160
+ INVALID_CHAIN_SEGMENT = "DOWNLOAD_BY_RANGE_ERROR_INVALID_CHAIN_SEGMENT",
1158
1161
  }
1159
1162
 
1160
1163
  export type DownloadByRangeErrorType =
@@ -1252,6 +1255,11 @@ export type DownloadByRangeErrorType =
1252
1255
  slot: Slot;
1253
1256
  expected: string;
1254
1257
  actual: string;
1258
+ }
1259
+ | {
1260
+ code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT;
1261
+ slot: Slot;
1262
+ reason: string;
1255
1263
  };
1256
1264
 
1257
1265
  export class DownloadByRangeError extends LodestarError<DownloadByRangeErrorType> {}