@lodestar/beacon-node 1.44.0-dev.4d5e0a5266 → 1.44.0-dev.51165b8806

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 (148) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +36 -33
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +0 -25
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/config/constants.d.ts +1 -0
  8. package/lib/api/impl/config/constants.d.ts.map +1 -1
  9. package/lib/api/impl/config/constants.js +2 -1
  10. package/lib/api/impl/config/constants.js.map +1 -1
  11. package/lib/api/impl/lodestar/index.d.ts.map +1 -1
  12. package/lib/api/impl/lodestar/index.js +28 -0
  13. package/lib/api/impl/lodestar/index.js.map +1 -1
  14. package/lib/api/impl/validator/index.d.ts.map +1 -1
  15. package/lib/api/impl/validator/index.js +20 -27
  16. package/lib/api/impl/validator/index.js.map +1 -1
  17. package/lib/chain/archiveStore/archiveStore.d.ts +0 -1
  18. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  19. package/lib/chain/archiveStore/archiveStore.js +0 -4
  20. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  21. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  22. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  23. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  24. package/lib/chain/chain.d.ts.map +1 -1
  25. package/lib/chain/chain.js +8 -1
  26. package/lib/chain/chain.js.map +1 -1
  27. package/lib/chain/emitter.d.ts +2 -1
  28. package/lib/chain/emitter.d.ts.map +1 -1
  29. package/lib/chain/emitter.js.map +1 -1
  30. package/lib/chain/errors/blockError.d.ts +0 -7
  31. package/lib/chain/errors/blockError.d.ts.map +1 -1
  32. package/lib/chain/errors/blockError.js +0 -3
  33. package/lib/chain/errors/blockError.js.map +1 -1
  34. package/lib/chain/errors/executionPayloadBid.d.ts +6 -0
  35. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  36. package/lib/chain/errors/executionPayloadBid.js +1 -0
  37. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  38. package/lib/chain/forkChoice/index.d.ts +4 -4
  39. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  40. package/lib/chain/forkChoice/index.js +10 -7
  41. package/lib/chain/forkChoice/index.js.map +1 -1
  42. package/lib/chain/options.d.ts.map +1 -1
  43. package/lib/chain/options.js +1 -0
  44. package/lib/chain/options.js.map +1 -1
  45. package/lib/chain/produceBlock/produceBlockBody.js +1 -1
  46. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  47. package/lib/chain/regen/interface.d.ts +0 -1
  48. package/lib/chain/regen/interface.d.ts.map +1 -1
  49. package/lib/chain/regen/queued.d.ts +0 -1
  50. package/lib/chain/regen/queued.d.ts.map +1 -1
  51. package/lib/chain/regen/queued.js +0 -4
  52. package/lib/chain/regen/queued.js.map +1 -1
  53. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +0 -5
  54. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  55. package/lib/chain/stateCache/fifoBlockStateCache.js +0 -5
  56. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  57. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +1 -4
  58. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  59. package/lib/chain/stateCache/persistentCheckpointsCache.js +5 -2
  60. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  61. package/lib/chain/stateCache/types.d.ts +0 -2
  62. package/lib/chain/stateCache/types.d.ts.map +1 -1
  63. package/lib/chain/stateCache/types.js.map +1 -1
  64. package/lib/chain/validation/block.d.ts +5 -1
  65. package/lib/chain/validation/block.d.ts.map +1 -1
  66. package/lib/chain/validation/block.js +4 -14
  67. package/lib/chain/validation/block.js.map +1 -1
  68. package/lib/chain/validation/executionPayloadBid.js +12 -1
  69. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  70. package/lib/chain/validation/executionPayloadEnvelope.js +0 -2
  71. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  72. package/lib/metrics/metrics/lodestar.d.ts +4 -0
  73. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  74. package/lib/metrics/metrics/lodestar.js +10 -0
  75. package/lib/metrics/metrics/lodestar.js.map +1 -1
  76. package/lib/network/gossip/topic.d.ts +750 -3
  77. package/lib/network/gossip/topic.d.ts.map +1 -1
  78. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  79. package/lib/network/processor/gossipHandlers.js +9 -2
  80. package/lib/network/processor/gossipHandlers.js.map +1 -1
  81. package/lib/network/processor/index.d.ts +2 -2
  82. package/lib/network/processor/index.d.ts.map +1 -1
  83. package/lib/network/processor/index.js +25 -23
  84. package/lib/network/processor/index.js.map +1 -1
  85. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  86. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +9 -5
  87. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  88. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  89. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +8 -3
  90. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  91. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +1 -1
  92. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  93. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts +2 -1
  94. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  95. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +16 -6
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  97. package/lib/network/reqresp/handlers/index.js +2 -2
  98. package/lib/network/reqresp/handlers/index.js.map +1 -1
  99. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  100. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +15 -5
  101. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  102. package/lib/sync/types.d.ts +9 -1
  103. package/lib/sync/types.d.ts.map +1 -1
  104. package/lib/sync/types.js +9 -2
  105. package/lib/sync/types.js.map +1 -1
  106. package/lib/sync/unknownBlock.d.ts.map +1 -1
  107. package/lib/sync/unknownBlock.js +64 -30
  108. package/lib/sync/unknownBlock.js.map +1 -1
  109. package/lib/util/dataColumns.d.ts.map +1 -1
  110. package/lib/util/dataColumns.js +16 -11
  111. package/lib/util/dataColumns.js.map +1 -1
  112. package/lib/util/sszBytes.js +1 -1
  113. package/package.json +15 -17
  114. package/src/api/impl/beacon/blocks/index.ts +40 -42
  115. package/src/api/impl/beacon/pool/index.ts +1 -41
  116. package/src/api/impl/config/constants.ts +2 -0
  117. package/src/api/impl/lodestar/index.ts +30 -0
  118. package/src/api/impl/validator/index.ts +29 -39
  119. package/src/chain/archiveStore/archiveStore.ts +0 -5
  120. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +2 -4
  121. package/src/chain/chain.ts +10 -1
  122. package/src/chain/emitter.ts +3 -2
  123. package/src/chain/errors/blockError.ts +0 -4
  124. package/src/chain/errors/executionPayloadBid.ts +7 -0
  125. package/src/chain/forkChoice/index.ts +13 -0
  126. package/src/chain/options.ts +1 -0
  127. package/src/chain/produceBlock/produceBlockBody.ts +1 -1
  128. package/src/chain/regen/interface.ts +0 -1
  129. package/src/chain/regen/queued.ts +0 -5
  130. package/src/chain/stateCache/fifoBlockStateCache.ts +0 -6
  131. package/src/chain/stateCache/persistentCheckpointsCache.ts +6 -2
  132. package/src/chain/stateCache/types.ts +0 -2
  133. package/src/chain/validation/block.ts +12 -16
  134. package/src/chain/validation/executionPayloadBid.ts +13 -1
  135. package/src/chain/validation/executionPayloadEnvelope.ts +0 -2
  136. package/src/metrics/metrics/lodestar.ts +11 -0
  137. package/src/network/processor/gossipHandlers.ts +9 -2
  138. package/src/network/processor/index.ts +27 -27
  139. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +12 -5
  140. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +11 -3
  141. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +1 -1
  142. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +22 -6
  143. package/src/network/reqresp/handlers/index.ts +2 -2
  144. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +14 -5
  145. package/src/sync/types.ts +11 -2
  146. package/src/sync/unknownBlock.ts +70 -31
  147. package/src/util/dataColumns.ts +17 -12
  148. package/src/util/sszBytes.ts +1 -1
@@ -185,7 +185,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
185
185
  peerIdStr,
186
186
  });
187
187
  try {
188
- await validateGossipBlock(config, chain, signedBlock, fork);
188
+ const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork);
189
189
 
190
190
  if (isForkPostGloas(fork)) {
191
191
  chain.seenPayloadEnvelopeInputCache.add({
@@ -205,8 +205,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
205
205
 
206
206
  metrics?.gossipBlock.gossipValidation.recvToValidation.observe(recvToValidation);
207
207
  metrics?.gossipBlock.gossipValidation.validationTime.observe(validationTime);
208
+ metrics?.gossipBlock.skippedSlots.observe(skippedSlots);
208
209
 
209
- logger.debug("Validated gossip block", {...blockInputMeta, ...logCtx, recvToValidation, validationTime});
210
+ logger.debug("Validated gossip block", {
211
+ ...blockInputMeta,
212
+ ...logCtx,
213
+ recvToValidation,
214
+ validationTime,
215
+ skippedSlots,
216
+ });
210
217
 
211
218
  chain.emitter.emit(routes.events.EventType.blockGossip, {slot, block: blockRootHex});
212
219
 
@@ -186,9 +186,11 @@ export class NetworkProcessor {
186
186
  // we may not receive the block for messages like Attestation and SignedAggregateAndProof messages, in that case PendingGossipsubMessage needs
187
187
  // to be stored in this Map and reprocessed once the block comes
188
188
  private readonly awaitingMessagesByBlockRoot: MapDef<RootHex, Set<PendingGossipsubMessage>>;
189
+ private awaitingBlockMessageCount = 0;
189
190
  // we may not receive the payload for messages that require the FULL payload variant to be processed,
190
191
  // in that case PendingGossipsubMessage needs to be stored in this Map and reprocessed once the payload comes
191
192
  private readonly awaitingMessagesByPayloadBlockRoot: MapDef<RootHex, Set<PendingGossipsubMessage>>;
193
+ private awaitingPayloadMessageCount = 0;
192
194
  private unknownBlocksBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
193
195
  private unknownEnvelopesBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
194
196
 
@@ -228,8 +230,8 @@ export class NetworkProcessor {
228
230
  metrics.gossipValidationQueue.keySize.set({topic}, this.gossipQueues[topic].keySize);
229
231
  metrics.gossipValidationQueue.concurrency.set({topic}, this.gossipTopicConcurrency[topic]);
230
232
  }
231
- metrics.awaitingBlockGossipMessages.countPerSlot.set(this.unknownBlockGossipsubMessagesCount);
232
- metrics.awaitingPayloadGossipMessages.countPerSlot.set(this.unknownPayloadGossipsubMessagesCount);
233
+ metrics.awaitingBlockGossipMessages.countPerSlot.set(this.awaitingBlockMessageCount);
234
+ metrics.awaitingPayloadGossipMessages.countPerSlot.set(this.awaitingPayloadMessageCount);
233
235
  // specific metric for beacon_attestation topic
234
236
  metrics.gossipValidationQueue.keyAge.reset();
235
237
  for (const ageMs of this.gossipQueues.beacon_attestation.getDataAgeMs()) {
@@ -297,7 +299,7 @@ export class NetworkProcessor {
297
299
  return;
298
300
  }
299
301
  this.unknownEnvelopesBySlot.getOrDefault(slot).add(root);
300
- this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, peer, source});
302
+ this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, slot, peer, source});
301
303
  }
302
304
 
303
305
  private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => {
@@ -497,7 +499,7 @@ export class NetworkProcessor {
497
499
  this.pushPendingGossipsubMessageToQueue(message);
498
500
  break;
499
501
  case PreprocessAction.AwaitBlock: {
500
- if (this.unknownBlockGossipsubMessagesCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
502
+ if (this.awaitingBlockMessageCount > MAX_QUEUED_UNKNOWN_BLOCK_GOSSIP_OBJECTS) {
501
503
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
502
504
  this.metrics?.awaitingBlockGossipMessages.reject.inc({
503
505
  reason: ReprocessRejectReason.reached_limit,
@@ -509,10 +511,11 @@ export class NetworkProcessor {
509
511
  this.metrics?.awaitingBlockGossipMessages.queue.inc({topic: topicType});
510
512
  const awaitingGossipsubMessages = this.awaitingMessagesByBlockRoot.getOrDefault(preprocessResult.root);
511
513
  awaitingGossipsubMessages.add(message);
514
+ this.awaitingBlockMessageCount++;
512
515
  break;
513
516
  }
514
517
  case PreprocessAction.AwaitEnvelope: {
515
- if (this.unknownPayloadGossipsubMessagesCount > MAX_QUEUED_UNKNOWN_PAYLOAD_GOSSIP_OBJECTS) {
518
+ if (this.awaitingPayloadMessageCount > MAX_QUEUED_UNKNOWN_PAYLOAD_GOSSIP_OBJECTS) {
516
519
  this.metrics?.awaitingPayloadGossipMessages.reject.inc({
517
520
  reason: ReprocessRejectReason.reached_limit,
518
521
  topic: topicType,
@@ -525,6 +528,7 @@ export class NetworkProcessor {
525
528
  preprocessResult.root
526
529
  );
527
530
  awaitingPayloadGossipsubMessages.add(message);
531
+ this.awaitingPayloadMessageCount++;
528
532
  break;
529
533
  }
530
534
  }
@@ -548,6 +552,12 @@ export class NetworkProcessor {
548
552
  return;
549
553
  }
550
554
 
555
+ // Atomically remove from map and update counter before async iteration to
556
+ // prevent double-decrement race with onClockSlot during yield points below
557
+ if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
558
+ this.awaitingBlockMessageCount -= waitingGossipsubMessages.size;
559
+ }
560
+
551
561
  const nowSec = Date.now() / 1000;
552
562
  let count = 0;
553
563
  // TODO: we can group attestations to process in batches but since we have the SeenAttestationDatas
@@ -567,8 +577,6 @@ export class NetworkProcessor {
567
577
  await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
568
578
  }
569
579
  }
570
-
571
- this.awaitingMessagesByBlockRoot.delete(rootHex);
572
580
  };
573
581
 
574
582
  private onPayloadEnvelopeProcessed = async ({blockRoot: rootHex}: {blockRoot: RootHex}): Promise<void> => {
@@ -577,6 +585,12 @@ export class NetworkProcessor {
577
585
  return;
578
586
  }
579
587
 
588
+ // Atomically remove from map and update counter before async iteration to
589
+ // prevent double-decrement race with onClockSlot during yield points below
590
+ if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
591
+ this.awaitingPayloadMessageCount -= waitingGossipsubMessages.size;
592
+ }
593
+
580
594
  const nowSec = Date.now() / 1000;
581
595
  let count = 0;
582
596
  for (const message of waitingGossipsubMessages) {
@@ -593,8 +607,6 @@ export class NetworkProcessor {
593
607
  await sleep(AWAITING_GOSSIP_OBJECTS_YIELD_EVERY_MS);
594
608
  }
595
609
  }
596
-
597
- this.awaitingMessagesByPayloadBlockRoot.delete(rootHex);
598
610
  };
599
611
 
600
612
  private onClockSlot = (clockSlot: Slot): void => {
@@ -618,7 +630,9 @@ export class NetworkProcessor {
618
630
  );
619
631
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
620
632
  }
621
- this.awaitingMessagesByBlockRoot.delete(rootHex);
633
+ if (this.awaitingMessagesByBlockRoot.delete(rootHex)) {
634
+ this.awaitingBlockMessageCount -= gossipMessages.size;
635
+ }
622
636
  }
623
637
  }
624
638
  this.unknownBlocksBySlot.delete(slot);
@@ -641,7 +655,9 @@ export class NetworkProcessor {
641
655
  );
642
656
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
643
657
  }
644
- this.awaitingMessagesByPayloadBlockRoot.delete(rootHex);
658
+ if (this.awaitingMessagesByPayloadBlockRoot.delete(rootHex)) {
659
+ this.awaitingPayloadMessageCount -= gossipMessages.size;
660
+ }
645
661
  }
646
662
  }
647
663
  this.unknownEnvelopesBySlot.delete(slot);
@@ -784,20 +800,4 @@ export class NetworkProcessor {
784
800
 
785
801
  return null;
786
802
  }
787
-
788
- private get unknownBlockGossipsubMessagesCount(): number {
789
- let count = 0;
790
- for (const messages of this.awaitingMessagesByBlockRoot.values()) {
791
- count += messages.size;
792
- }
793
- return count;
794
- }
795
-
796
- private get unknownPayloadGossipsubMessagesCount(): number {
797
- let count = 0;
798
- for (const messages of this.awaitingMessagesByPayloadBlockRoot.values()) {
799
- count += messages.size;
800
- }
801
- return count;
802
- }
803
803
  }
@@ -1,6 +1,6 @@
1
1
  import {PeerId} from "@libp2p/interface";
2
2
  import {BeaconConfig} from "@lodestar/config";
3
- import {GENESIS_SLOT, isForkPostDeneb, isForkPostFulu} from "@lodestar/params";
3
+ import {GENESIS_SLOT, isForkPostDeneb} from "@lodestar/params";
4
4
  import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
5
  import {computeEpochAtSlot} from "@lodestar/state-transition";
6
6
  import {deneb, phase0} from "@lodestar/types";
@@ -29,13 +29,20 @@ export async function* onBeaconBlocksByRange(
29
29
  // starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
30
30
  const archiveMaxSlot = finalizedSlot;
31
31
 
32
- const forkName = chain.config.getForkName(startSlot);
33
- if (isForkPostFulu(forkName) && startSlot < chain.earliestAvailableSlot) {
34
- chain.logger.verbose("Peer did not respect earliestAvailableSlot for BeaconBlocksByRange", {
32
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
33
+ // Throw only when the entire requested range is below earliestAvailableSlot.
34
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
35
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for BeaconBlocksByRange", {
35
36
  peer: prettyPrintPeerId(peerId),
36
37
  client: peerClient,
38
+ startSlot,
39
+ count,
40
+ earliestAvailableSlot: chain.earliestAvailableSlot,
37
41
  });
38
- return;
42
+ throw new ResponseError(
43
+ RespStatus.RESOURCE_UNAVAILABLE,
44
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
45
+ );
39
46
  }
40
47
 
41
48
  // Finalized range of blocks
@@ -34,12 +34,20 @@ export async function* onDataColumnSidecarsByRange(
34
34
  return;
35
35
  }
36
36
 
37
- if (startSlot < chain.earliestAvailableSlot) {
38
- chain.logger.verbose("Peer did not respect earliestAvailableSlot for DataColumnSidecarsByRange", {
37
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
38
+ // Throw only when the entire requested range is below earliestAvailableSlot.
39
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
40
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for DataColumnSidecarsByRange", {
39
41
  peer: prettyPrintPeerId(peerId),
40
42
  client: peerClient,
43
+ startSlot,
44
+ count,
45
+ earliestAvailableSlot: chain.earliestAvailableSlot,
41
46
  });
42
- return;
47
+ throw new ResponseError(
48
+ RespStatus.RESOURCE_UNAVAILABLE,
49
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
50
+ );
43
51
  }
44
52
 
45
53
  const finalized = db.dataColumnSidecarArchive;
@@ -30,7 +30,7 @@ export async function* onDataColumnSidecarsByRoot(
30
30
  const {blockRoot, columns: requestedColumns} = dataColumnsByRootIdentifier;
31
31
  const availableColumns = validateRequestedDataColumns(chain, requestedColumns);
32
32
  if (availableColumns.length === 0) {
33
- return;
33
+ continue;
34
34
  }
35
35
 
36
36
  const blockRootHex = toRootHex(blockRoot);
@@ -1,3 +1,4 @@
1
+ import {PeerId} from "@libp2p/interface";
1
2
  import {ChainConfig} from "@lodestar/config";
2
3
  import {PayloadStatus} from "@lodestar/fork-choice";
3
4
  import {GENESIS_SLOT} from "@lodestar/params";
@@ -6,23 +7,38 @@ import {computeEpochAtSlot} from "@lodestar/state-transition";
6
7
  import {gloas} from "@lodestar/types";
7
8
  import {IBeaconChain} from "../../../chain/index.js";
8
9
  import {IBeaconDb} from "../../../db/index.js";
10
+ import {prettyPrintPeerId} from "../../util.js";
9
11
 
10
12
  export async function* onExecutionPayloadEnvelopesByRange(
11
13
  request: gloas.ExecutionPayloadEnvelopesByRangeRequest,
12
14
  chain: IBeaconChain,
13
- db: IBeaconDb
15
+ db: IBeaconDb,
16
+ peerId: PeerId,
17
+ peerClient: string
14
18
  ): AsyncIterable<ResponseOutgoing> {
15
19
  const {startSlot, count} = validateExecutionPayloadEnvelopesByRangeRequest(chain.config, request);
16
20
  const endSlot = startSlot + count;
17
21
 
18
- if (startSlot < chain.earliestAvailableSlot) {
19
- return;
22
+ // endSlot is exclusive, so highest served slot is endSlot - 1.
23
+ // Throw only when the entire requested range is below earliestAvailableSlot.
24
+ if (endSlot - 1 < chain.earliestAvailableSlot) {
25
+ chain.logger.verbose("Peer requested range before earliestAvailableSlot for ExecutionPayloadEnvelopesByRange", {
26
+ peer: prettyPrintPeerId(peerId),
27
+ client: peerClient,
28
+ startSlot,
29
+ count,
30
+ earliestAvailableSlot: chain.earliestAvailableSlot,
31
+ });
32
+ throw new ResponseError(
33
+ RespStatus.RESOURCE_UNAVAILABLE,
34
+ `Requested range is before earliestAvailableSlot startSlot=${startSlot} count=${count} earliestAvailableSlot=${chain.earliestAvailableSlot}`
35
+ );
20
36
  }
21
37
 
22
38
  const finalized = db.executionPayloadEnvelopeArchive;
23
- const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot();
24
- // The current finalized block's envelope is still in the hot db; archive migration happens
25
- // in the next finalization run (see migrateExecutionPayloadEnvelopesFromHotToColdDb).
39
+ // Use the finalized block's actual slot as the checkpoint epoch-boundary slot may be skipped
40
+ const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot;
41
+ // The finalized block's envelope stays in the hot db until the next finalization run
26
42
  const archiveMaxSlot = finalizedSlot - 1;
27
43
 
28
44
  // Finalized range of envelopes
@@ -74,9 +74,9 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
74
74
  const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data);
75
75
  return onExecutionPayloadEnvelopesByRoot(body, chain, db, peerId, peerClient);
76
76
  },
77
- [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req) => {
77
+ [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => {
78
78
  const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
79
- return onExecutionPayloadEnvelopesByRange(body, chain, db);
79
+ return onExecutionPayloadEnvelopesByRange(body, chain, db, peerId, peerClient);
80
80
  },
81
81
 
82
82
  [ReqRespMethod.LightClientBootstrap]: (req) => {
@@ -1,6 +1,5 @@
1
1
  import {LogData} from "@lodestar/logger";
2
2
  import {ForkSeq} from "@lodestar/params";
3
- import {RespStatus, ResponseError} from "@lodestar/reqresp";
4
3
  import {ColumnIndex, Slot} from "@lodestar/types";
5
4
  import {prettyBytes, prettyPrintIndices, toRootHex} from "@lodestar/utils";
6
5
  import {IBeaconChain} from "../../../chain/interface.js";
@@ -76,12 +75,22 @@ export async function handleColumnSidecarUnavailability({
76
75
 
77
76
  export function validateRequestedDataColumns(chain: IBeaconChain, requestedColumns: ColumnIndex[]): ColumnIndex[] {
78
77
  if (requestedColumns.length === 0) {
79
- throw new ResponseError(RespStatus.INVALID_REQUEST, "dataColumnSidecar requested without column indices");
78
+ return [];
80
79
  }
81
80
 
82
- const custodyColumns = chain.custodyConfig.custodyColumns;
83
- const availableColumns = requestedColumns.filter((c) => custodyColumns.includes(c));
84
- const missingColumns = requestedColumns.filter((c) => !custodyColumns.includes(c));
81
+ const {custodyColumns, custodyColumnsIndex} = chain.custodyConfig;
82
+ const availableColumns: ColumnIndex[] = [];
83
+ const missingColumns: ColumnIndex[] = [];
84
+ for (const c of requestedColumns) {
85
+ // `c` is peer-controlled and SSZ-deserialized as `uint64`, so it may exceed
86
+ // `NUMBER_OF_COLUMNS - 1`; `Uint8Array` returns `undefined` for OOB reads,
87
+ // and `undefined !== 0` would silently classify OOB indices as custodied.
88
+ if ((custodyColumnsIndex[c] ?? 0) !== 0) {
89
+ availableColumns.push(c);
90
+ } else {
91
+ missingColumns.push(c);
92
+ }
93
+ }
85
94
 
86
95
  if (missingColumns.length > 0) {
87
96
  chain.logger.verbose("Requested dataColumnSidecar for non-custody columns", {
package/src/sync/types.ts CHANGED
@@ -19,7 +19,14 @@ export enum PendingBlockType {
19
19
  */
20
20
  INCOMPLETE_BLOCK_INPUT = "IncompleteBlockInput",
21
21
 
22
- UNKNOWN_DATA = "unknown_data",
22
+ /**
23
+ * Payload analog of UNKNOWN_BLOCK_ROOT: we have a beacon block root but not its execution payload envelope.
24
+ */
25
+ UNKNOWN_PAYLOAD_BLOCK_ROOT = "unknown_payload_block_root",
26
+ /**
27
+ * Payload analog of INCOMPLETE_BLOCK_INPUT: we have a partial payload input that did not complete in time.
28
+ */
29
+ INCOMPLETE_PAYLOAD_ENVELOPE = "incomplete_payload_envelope",
23
30
  }
24
31
 
25
32
  export enum PendingBlockInputStatus {
@@ -70,6 +77,8 @@ export type PendingPayloadInput = {
70
77
  export type PendingPayloadRootHex = {
71
78
  status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching;
72
79
  rootHex: RootHex;
80
+ // message slot hint, may be missing when resolving a parent payload
81
+ slot?: Slot;
73
82
  timeAddedSec: number;
74
83
  timeSyncedSec?: number;
75
84
  peerIdStrings: Set<string>;
@@ -125,5 +134,5 @@ export function getPayloadSyncCacheItemSlot(payload: PayloadSyncCacheItem): Slot
125
134
  return payload.envelope.message.payload.slotNumber;
126
135
  }
127
136
 
128
- return "unknown";
137
+ return payload.slot ?? "unknown";
129
138
  }
@@ -3,7 +3,7 @@ import {ChainForkConfig} from "@lodestar/config";
3
3
  import {ForkSeq} from "@lodestar/params";
4
4
  import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
5
5
  import {computeTimeAtSlot} from "@lodestar/state-transition";
6
- import {RootHex, gloas} from "@lodestar/types";
6
+ import {RootHex, Slot, gloas} from "@lodestar/types";
7
7
  import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils";
8
8
  import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js";
9
9
  import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js";
@@ -215,10 +215,10 @@ export class BlockInputSync {
215
215
 
216
216
  private onUnknownEnvelopeBlockRoot = (data: ChainEventData[ChainEvent.unknownEnvelopeBlockRoot]): void => {
217
217
  try {
218
- this.addByPayloadRootHex(data.rootHex, data.peer);
218
+ this.addByPayloadRootHex(data.rootHex, data.peer, data.slot);
219
219
  this.triggerUnknownBlockSearch();
220
- this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
221
- this.metrics?.blockInputSync.source.inc({source: data.source});
220
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_PAYLOAD_BLOCK_ROOT});
221
+ this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
222
222
  } catch (e) {
223
223
  this.logger.debug("Error handling unknownEnvelopeBlockRoot event", {}, e as Error);
224
224
  }
@@ -228,8 +228,8 @@ export class BlockInputSync {
228
228
  try {
229
229
  this.addByPayloadInput(data.payloadInput, data.peer);
230
230
  this.triggerUnknownBlockSearch();
231
- this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.UNKNOWN_DATA});
232
- this.metrics?.blockInputSync.source.inc({source: data.source});
231
+ this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.INCOMPLETE_PAYLOAD_ENVELOPE});
232
+ this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
233
233
  } catch (e) {
234
234
  this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e as Error);
235
235
  }
@@ -331,7 +331,10 @@ export class BlockInputSync {
331
331
  };
332
332
  this.pendingBlocks.set(blockInput.blockRootHex, pendingBlock);
333
333
 
334
- this.logger.verbose("Added blockInput to BlockInputSync.pendingBlocks", pendingBlock.blockInput.getLogMeta());
334
+ this.logger.verbose("Added blockInput to BlockInputSync.pendingBlocks", {
335
+ ...pendingBlock.blockInput.getLogMeta(),
336
+ delaySec: this.chain.clock.secFromSlot(blockInput.slot),
337
+ });
335
338
  }
336
339
 
337
340
  if (peerIdStr) {
@@ -346,13 +349,14 @@ export class BlockInputSync {
346
349
  }
347
350
  };
348
351
 
349
- private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
352
+ private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr, slot?: Slot): boolean => {
350
353
  let pendingPayload = this.pendingPayloads.get(rootHex);
351
354
  let added = false;
352
355
  if (!pendingPayload) {
353
356
  pendingPayload = {
354
357
  status: PendingPayloadInputStatus.pending,
355
358
  rootHex,
359
+ slot,
356
360
  peerIdStrings: new Set(),
357
361
  timeAddedSec: Date.now() / 1000,
358
362
  };
@@ -360,6 +364,8 @@ export class BlockInputSync {
360
364
  added = true;
361
365
 
362
366
  this.logger.verbose("Added new payload rootHex to BlockInputSync.pendingPayloads", {
367
+ slot: slot ?? "unknown",
368
+ delaySec: slot != null ? this.chain.clock.secFromSlot(slot) : "unknown",
363
369
  root: rootHex,
364
370
  peerIdStr: peerIdStr ?? "unknown peer",
365
371
  });
@@ -392,6 +398,13 @@ export class BlockInputSync {
392
398
  }
393
399
 
394
400
  this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
401
+
402
+ this.logger.verbose("Added payloadInput to BlockInputSync.pendingPayloads", {
403
+ slot: payloadInput.slot,
404
+ root: payloadInput.blockRootHex,
405
+ delaySec: this.chain.clock.secFromSlot(payloadInput.slot),
406
+ });
407
+
395
408
  const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
396
409
  if (prunedItemCount > 0) {
397
410
  this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
@@ -654,10 +667,12 @@ export class BlockInputSync {
654
667
  }
655
668
 
656
669
  const rootHex = getBlockInputSyncCacheItemRootHex(block);
670
+ const blockSlot = getBlockInputSyncCacheItemSlot(block);
657
671
  const logCtx = {
658
- slot: getBlockInputSyncCacheItemSlot(block),
672
+ slot: blockSlot,
659
673
  root: rootHex,
660
674
  pendingBlocks: this.pendingBlocks.size,
675
+ ...(typeof blockSlot === "number" && {delaySec: this.chain.clock.secFromSlot(blockSlot)}),
661
676
  };
662
677
 
663
678
  this.logger.verbose("BlockInputSync.downloadBlock()", logCtx);
@@ -679,6 +694,7 @@ export class BlockInputSync {
679
694
  const logCtx2 = {
680
695
  ...logCtx,
681
696
  slot: blockSlot,
697
+ delaySec,
682
698
  parentInForkChoice,
683
699
  };
684
700
  this.logger.verbose("Downloaded unknown block", logCtx2);
@@ -748,6 +764,12 @@ export class BlockInputSync {
748
764
  // this prevents unbundling attack
749
765
  // see https://lighthouse-blog.sigmaprime.io/mev-unbundling-rpc.html
750
766
  const {slot: blockSlot, proposerIndex} = pendingBlock.blockInput.getBlock().message;
767
+ const logCtx = {
768
+ slot: blockSlot,
769
+ root: pendingBlock.blockInput.blockRootHex,
770
+ delaySec: this.chain.clock.secFromSlot(blockSlot),
771
+ };
772
+ this.logger.verbose("Processing downloaded block", logCtx);
751
773
  const fork = this.config.getForkName(blockSlot);
752
774
  const proposerBoostWindowMs = this.config.getAttestationDueMs(fork);
753
775
  if (
@@ -783,6 +805,7 @@ export class BlockInputSync {
783
805
  else this.metrics?.blockInputSync.processedBlocksSuccess.inc();
784
806
 
785
807
  if (!res.err) {
808
+ this.logger.verbose("Processed block from unknown sync", logCtx);
786
809
  // no need to update status to "processed", delete anyway
787
810
  this.pendingBlocks.delete(pendingBlock.blockInput.blockRootHex);
788
811
  // Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
@@ -850,25 +873,28 @@ export class BlockInputSync {
850
873
  }
851
874
 
852
875
  const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
853
- if (!payloadInput) {
854
- if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
855
- // Column commitments live on the block body, so an envelope-only entry has to pull the block first.
856
- if (!this.pendingBlocks.has(rootHex)) {
857
- this.addByRootHex(rootHex);
858
- }
876
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
877
+ // Block not in fork choice yet. payloadInput may be seeded from the block body during download, so a
878
+ // non-null payloadInput does not imply the block is imported; defer regardless and pull the block first.
879
+ // onBlockImported re-triggers the search to resume this envelope.
880
+ if (!this.pendingBlocks.has(rootHex)) {
881
+ this.addByRootHex(rootHex);
882
+ }
859
883
 
860
- const pendingBlock = this.pendingBlocks.get(rootHex);
861
- if (pendingBlock && this.network.getConnectedPeers().length > 0) {
862
- await this.downloadBlock(pendingBlock);
863
- }
864
- } else {
865
- this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
866
- root: rootHex,
867
- });
884
+ const pendingBlock = this.pendingBlocks.get(rootHex);
885
+ if (pendingBlock && this.network.getConnectedPeers().length > 0) {
886
+ await this.downloadBlock(pendingBlock);
868
887
  }
869
888
  return;
870
889
  }
871
890
 
891
+ if (!payloadInput) {
892
+ this.logger.debug("Missing PayloadEnvelopeInput for known block while reconciling payload envelope", {
893
+ root: rootHex,
894
+ });
895
+ return;
896
+ }
897
+
872
898
  if (!payloadInput.hasPayloadEnvelope()) {
873
899
  const validationResult = await wrapError(
874
900
  validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope)
@@ -922,10 +948,12 @@ export class BlockInputSync {
922
948
  return;
923
949
  }
924
950
 
951
+ const payloadSlot = getPayloadSyncCacheItemSlot(payload);
925
952
  const logCtx = {
926
- slot: getPayloadSyncCacheItemSlot(payload),
953
+ slot: payloadSlot,
927
954
  root: rootHex,
928
955
  pendingPayloads: this.pendingPayloads.size,
956
+ ...(typeof payloadSlot === "number" && {delaySec: this.chain.clock.secFromSlot(payloadSlot)}),
929
957
  };
930
958
 
931
959
  this.logger.verbose("BlockInputSync.downloadPayload()", logCtx);
@@ -953,7 +981,11 @@ export class BlockInputSync {
953
981
 
954
982
  private async processPayload(pendingPayload: PendingPayloadInput): Promise<void> {
955
983
  const rootHex = pendingPayload.payloadInput.blockRootHex;
956
- const logCtx = {slot: pendingPayload.payloadInput.slot, root: rootHex};
984
+ const logCtx = {
985
+ slot: pendingPayload.payloadInput.slot,
986
+ root: rootHex,
987
+ delaySec: this.chain.clock.secFromSlot(pendingPayload.payloadInput.slot),
988
+ };
957
989
 
958
990
  if (pendingPayload.status !== PendingPayloadInputStatus.downloaded) {
959
991
  this.logger.debug("Skipping payload processing before payload input is downloaded", {
@@ -980,6 +1012,7 @@ export class BlockInputSync {
980
1012
  }
981
1013
 
982
1014
  pendingPayload.status = PendingPayloadInputStatus.processing;
1015
+ this.logger.debug("Processing downloaded payload", logCtx);
983
1016
 
984
1017
  const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput));
985
1018
  if (!res.err) {
@@ -1073,11 +1106,11 @@ export class BlockInputSync {
1073
1106
  }
1074
1107
 
1075
1108
  payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex);
1076
- if (!payloadInput) {
1077
- if (this.chain.forkChoice.hasBlockHex(rootHex)) {
1078
- throw new Error(`Missing PayloadEnvelopeInput for known block ${rootHex}`);
1079
- }
1080
- // Keep the validated envelope around, but wait for the block body before turning it into a full payload input.
1109
+ if (!this.chain.forkChoice.hasBlockHex(rootHex)) {
1110
+ // Block not in fork choice yet. Validating now would throw BLOCK_ROOT_UNKNOWN, so keep the downloaded
1111
+ // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
1112
+ // payloadInput may be seeded from the block body during download, so a non-null payloadInput does not
1113
+ // imply the block is imported.
1081
1114
  return {
1082
1115
  status: PendingPayloadInputStatus.waitingForBlock,
1083
1116
  envelope,
@@ -1086,6 +1119,11 @@ export class BlockInputSync {
1086
1119
  };
1087
1120
  }
1088
1121
 
1122
+ if (!payloadInput) {
1123
+ // Block is in fork choice but no PayloadEnvelopeInput exists, should have been created during block import.
1124
+ throw new Error(`Missing PayloadEnvelopeInput for known block ${rootHex}`);
1125
+ }
1126
+
1089
1127
  if (!payloadInput.hasPayloadEnvelope()) {
1090
1128
  await validateGossipExecutionPayloadEnvelope(this.chain, envelope);
1091
1129
  }
@@ -1117,6 +1155,7 @@ export class BlockInputSync {
1117
1155
  rootHex,
1118
1156
  peerId,
1119
1157
  peerClient,
1158
+ ...(typeof slot === "number" && {delaySec: this.chain.clock.secFromSlot(slot)}),
1120
1159
  hasPayload: pendingPayload.payloadInput.hasPayloadEnvelope(),
1121
1160
  hasAllData: pendingPayload.payloadInput.hasAllData(),
1122
1161
  });
@@ -1288,7 +1327,7 @@ export class BlockInputSync {
1288
1327
  this.metrics?.blockInputSync.fetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
1289
1328
  }
1290
1329
 
1291
- const logCtx = {slot, rootHex, peerId, peerClient};
1330
+ const logCtx = {slot, rootHex, peerId, peerClient, delaySec: this.chain.clock.secFromSlot(slot)};
1292
1331
  this.logger.verbose("BlockInputSync.fetchBlockInput: successful download", logCtx);
1293
1332
  this.metrics?.blockInputSync.downloadByRoot.success.inc();
1294
1333
  const warnings = downloadResult.warnings;