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

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.
Files changed (101) hide show
  1. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/pool/index.js +46 -5
  3. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +17 -10
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  8. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  9. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  10. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  11. package/lib/chain/chain.d.ts +2 -1
  12. package/lib/chain/chain.d.ts.map +1 -1
  13. package/lib/chain/chain.js +3 -1
  14. package/lib/chain/chain.js.map +1 -1
  15. package/lib/chain/errors/executionPayloadBid.d.ts +19 -1
  16. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  17. package/lib/chain/errors/executionPayloadBid.js +3 -0
  18. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  19. package/lib/chain/interface.d.ts +2 -1
  20. package/lib/chain/interface.d.ts.map +1 -1
  21. package/lib/chain/interface.js.map +1 -1
  22. package/lib/chain/lightClient/index.d.ts.map +1 -1
  23. package/lib/chain/lightClient/index.js +1 -1
  24. package/lib/chain/lightClient/index.js.map +1 -1
  25. package/lib/chain/opPools/index.d.ts +1 -0
  26. package/lib/chain/opPools/index.d.ts.map +1 -1
  27. package/lib/chain/opPools/index.js +1 -0
  28. package/lib/chain/opPools/index.js.map +1 -1
  29. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  30. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  31. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  32. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  33. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  34. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  35. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  36. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  37. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  38. package/lib/chain/validation/executionPayloadBid.js +64 -16
  39. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  40. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  41. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  42. package/lib/chain/validation/payloadAttestationMessage.js +5 -3
  43. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  44. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  45. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  46. package/lib/metrics/metrics/lodestar.js +5 -0
  47. package/lib/metrics/metrics/lodestar.js.map +1 -1
  48. package/lib/network/interface.d.ts +1 -0
  49. package/lib/network/interface.d.ts.map +1 -1
  50. package/lib/network/network.d.ts +1 -0
  51. package/lib/network/network.d.ts.map +1 -1
  52. package/lib/network/network.js +5 -0
  53. package/lib/network/network.js.map +1 -1
  54. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  55. package/lib/network/processor/gossipHandlers.js +39 -4
  56. package/lib/network/processor/gossipHandlers.js.map +1 -1
  57. package/lib/sync/range/batch.d.ts +20 -3
  58. package/lib/sync/range/batch.d.ts.map +1 -1
  59. package/lib/sync/range/batch.js +54 -10
  60. package/lib/sync/range/batch.js.map +1 -1
  61. package/lib/sync/range/chain.d.ts.map +1 -1
  62. package/lib/sync/range/chain.js +3 -1
  63. package/lib/sync/range/chain.js.map +1 -1
  64. package/lib/sync/range/range.d.ts.map +1 -1
  65. package/lib/sync/range/range.js +40 -2
  66. package/lib/sync/range/range.js.map +1 -1
  67. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  68. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  69. package/lib/sync/range/utils/peerBalancer.js +8 -4
  70. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  71. package/lib/sync/utils/downloadByRange.d.ts +7 -1
  72. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  73. package/lib/sync/utils/downloadByRange.js +2 -0
  74. package/lib/sync/utils/downloadByRange.js.map +1 -1
  75. package/lib/util/dependentRoot.d.ts +6 -2
  76. package/lib/util/dependentRoot.d.ts.map +1 -1
  77. package/lib/util/dependentRoot.js +20 -16
  78. package/lib/util/dependentRoot.js.map +1 -1
  79. package/package.json +14 -15
  80. package/src/api/impl/beacon/pool/index.ts +56 -3
  81. package/src/api/impl/validator/index.ts +18 -10
  82. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  83. package/src/chain/chain.ts +3 -0
  84. package/src/chain/errors/executionPayloadBid.ts +22 -1
  85. package/src/chain/interface.ts +2 -0
  86. package/src/chain/lightClient/index.ts +6 -6
  87. package/src/chain/opPools/index.ts +1 -0
  88. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  89. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  90. package/src/chain/validation/executionPayloadBid.ts +67 -17
  91. package/src/chain/validation/payloadAttestationMessage.ts +6 -4
  92. package/src/metrics/metrics/lodestar.ts +6 -0
  93. package/src/network/interface.ts +1 -0
  94. package/src/network/network.ts +11 -0
  95. package/src/network/processor/gossipHandlers.ts +43 -3
  96. package/src/sync/range/batch.ts +71 -11
  97. package/src/sync/range/chain.ts +3 -1
  98. package/src/sync/range/range.ts +52 -2
  99. package/src/sync/range/utils/peerBalancer.ts +9 -3
  100. package/src/sync/utils/downloadByRange.ts +8 -0
  101. package/src/util/dependentRoot.ts +22 -18
@@ -3,6 +3,7 @@ import {NotReorgedReason} from "@lodestar/fork-choice";
3
3
  import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
+ import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
6
7
  import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
7
8
  import {JobQueueItemType} from "../../chain/bls/index.js";
8
9
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
@@ -890,6 +891,11 @@ export function createLodestarMetrics(
890
891
  labelNames: ["source"],
891
892
  buckets: [0.5, 1, 2, 4, 6, 12],
892
893
  }),
894
+ processPayloadErrors: register.gauge<{error: PayloadErrorCode | "NOT_PAYLOAD_ERROR"}>({
895
+ name: "lodestar_gossip_execution_payload_envelope_process_payload_errors",
896
+ help: "Count of errors, by error type, while processing execution payload envelopes",
897
+ labelNames: ["error"],
898
+ }),
893
899
  },
894
900
  // recovery in the case of specific blob rows required
895
901
  recoverBlobSidecars: {
@@ -114,6 +114,7 @@ export interface INetwork extends INetworkCorePublic {
114
114
  publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number>;
115
115
  publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
116
116
  publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
117
+ publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
117
118
 
118
119
  // Debug
119
120
  dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]>;
@@ -526,6 +526,17 @@ export class Network implements INetwork {
526
526
  );
527
527
  }
528
528
 
529
+ async publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number> {
530
+ const epoch = computeEpochAtSlot(signedProposerPreferences.message.proposalSlot);
531
+ const boundary = this.config.getForkBoundaryAtEpoch(epoch);
532
+
533
+ return this.publishGossip<GossipType.proposer_preferences>(
534
+ {type: GossipType.proposer_preferences, boundary},
535
+ signedProposerPreferences,
536
+ {ignoreDuplicatePublishError: true}
537
+ );
538
+ }
539
+
529
540
  private async publishGossip<K extends GossipType>(
530
541
  topic: GossipTopicMap[K],
531
542
  object: GossipTypeMap[K],
@@ -35,6 +35,7 @@ import {
35
35
  IBlockInput,
36
36
  isBlockInputColumns,
37
37
  } from "../../chain/blocks/blockInput/index.js";
38
+ import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
38
39
  import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
39
40
  import {BlobSidecarValidation} from "../../chain/blocks/types.js";
40
41
  import {ChainEvent} from "../../chain/emitter.js";
@@ -1148,7 +1149,40 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1148
1149
  });
1149
1150
 
1150
1151
  chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1151
- chain.logger.debug("Error processing execution payload from gossip", {slot, root: blockRootHex}, e as Error);
1152
+ // Adjust verbosity based on error type
1153
+ let logLevel: LogLevel;
1154
+
1155
+ if (e instanceof PayloadError) {
1156
+ switch (e.type.code) {
1157
+ // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
1158
+ // already verified the block is in fork choice
1159
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
1160
+ case PayloadErrorCode.MISS_BLOCK_STATE:
1161
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1162
+ // Errors might indicate an issue with our node or the connected EL client
1163
+ logLevel = LogLevel.error;
1164
+ break;
1165
+ // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1166
+ case PayloadErrorCode.INVALID_SIGNATURE:
1167
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1168
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1169
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1170
+ // Misbehaving peer, but could highlight an issue in another client
1171
+ logLevel = LogLevel.warn;
1172
+ break;
1173
+ }
1174
+ } else {
1175
+ // Any unexpected error
1176
+ logLevel = LogLevel.error;
1177
+ }
1178
+ metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
1179
+ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
1180
+ });
1181
+ chain.logger[logLevel](
1182
+ "Error processing execution payload from gossip",
1183
+ {slot, peer: peerIdStr, root: blockRootHex},
1184
+ e as Error
1185
+ );
1152
1186
  });
1153
1187
  },
1154
1188
  [GossipType.payload_attestation_message]: async ({
@@ -1163,7 +1197,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1163
1197
  const insertOutcome = chain.payloadAttestationPool.add(
1164
1198
  payloadAttestationMessage,
1165
1199
  validationResult.attDataRootHex,
1166
- validationResult.validatorCommitteeIndex
1200
+ validationResult.validatorCommitteeIndices
1167
1201
  );
1168
1202
  metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1169
1203
  } catch (e) {
@@ -1171,7 +1205,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1171
1205
  }
1172
1206
  chain.forkChoice.notifyPtcMessages(
1173
1207
  toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1174
- [validationResult.validatorCommitteeIndex],
1208
+ validationResult.validatorCommitteeIndices,
1175
1209
  payloadAttestationMessage.data.payloadPresent
1176
1210
  );
1177
1211
  },
@@ -1203,6 +1237,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1203
1237
  const {serializedData} = gossipData;
1204
1238
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
1205
1239
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
1240
+
1241
+ chain.proposerPreferencesPool.add(signedProposerPreferences);
1242
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1243
+ version: ForkName.gloas,
1244
+ data: signedProposerPreferences,
1245
+ });
1206
1246
  },
1207
1247
  };
1208
1248
  }
@@ -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> {}
@@ -2,45 +2,49 @@ import {EpochDifference, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {Epoch, RootHex} from "@lodestar/types";
3
3
 
4
4
  /**
5
- * Get dependent root of a shuffling given attestation epoch and head block.
5
+ * Get dependent root of a shuffling given a message epoch and a proto block.
6
+ *
7
+ * Pre-gloas, this is used for attestation validation
8
+ * Post-gloas, this is also used for execution_payload_bid validation because post-fulu,
9
+ * a dependent root of a proposal duties is 1-epoch look ahead (instead of 0 as of pre-fulu)
6
10
  */
7
11
  export function getShufflingDependentRoot(
8
12
  forkChoice: IForkChoice,
9
- attEpoch: Epoch,
10
- blockEpoch: Epoch,
11
- attHeadBlock: ProtoBlock
13
+ msgEpoch: Epoch,
14
+ protoBlockEpoch: Epoch,
15
+ protoBlock: ProtoBlock
12
16
  ): RootHex {
13
17
  let shufflingDependentRoot: RootHex;
14
- if (blockEpoch === attEpoch) {
18
+ if (protoBlockEpoch === msgEpoch) {
15
19
  // current shuffling, this is equivalent to `headState.currentShuffling`
16
- // given blockEpoch = attEpoch = n
20
+ // given protoBlockEpoch = msgEpoch = n
17
21
  // epoch: (n-2) (n-1) n (n+1)
18
22
  // |-------|-------|-------|-------|
19
- // attHeadBlock ------------------------^
23
+ // protoBlock ------------------------^
20
24
  // shufflingDependentRoot ------^
21
- shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.previous);
22
- } else if (blockEpoch === attEpoch - 1) {
25
+ shufflingDependentRoot = forkChoice.getDependentRoot(protoBlock, EpochDifference.previous);
26
+ } else if (protoBlockEpoch === msgEpoch - 1) {
23
27
  // next shuffling, this is equivalent to `headState.nextShuffling`
24
- // given blockEpoch = n-1, attEpoch = n
28
+ // given protoBlockEpoch = n-1, msgEpoch = n
25
29
  // epoch: (n-2) (n-1) n (n+1)
26
30
  // |-------|-------|-------|-------|
27
- // attHeadBlock -------------------^
31
+ // protoBlock -------------------^
28
32
  // shufflingDependentRoot ------^
29
- shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.current);
30
- } else if (blockEpoch < attEpoch - 1) {
33
+ shufflingDependentRoot = forkChoice.getDependentRoot(protoBlock, EpochDifference.current);
34
+ } else if (protoBlockEpoch < msgEpoch - 1) {
31
35
  // this never happens with default chain option of maxSkipSlots = 32, however we still need to handle it
32
36
  // check the verifyHeadBlockAndTargetRoot() function above
33
- // given blockEpoch = n-2, attEpoch = n
37
+ // given protoBlockEpoch = n-2, msgEpoch = n
34
38
  // epoch: (n-2) (n-1) n (n+1)
35
39
  // |-------|-------|-------|-------|
36
- // attHeadBlock -----------^
40
+ // protoBlock -----------^
37
41
  // shufflingDependentRoot -----^
38
- shufflingDependentRoot = attHeadBlock.blockRoot;
42
+ shufflingDependentRoot = protoBlock.blockRoot;
39
43
  // use lodestar_gossip_attestation_head_slot_to_attestation_slot metric to track this case
40
44
  } else {
41
- // blockEpoch > attEpoch
45
+ // protoBlockEpoch > msgEpoch
42
46
  // should not happen, handled in verifyAttestationTargetRoot
43
- throw Error(`attestation epoch ${attEpoch} is before head block epoch ${blockEpoch}`);
47
+ throw Error(`message epoch ${msgEpoch} is before proto block epoch ${protoBlockEpoch}`);
44
48
  }
45
49
 
46
50
  return shufflingDependentRoot;