@lodestar/beacon-node 1.43.0-dev.ca1fc40294 → 1.43.0-dev.dfb984e779

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 (170) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +3 -2
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/lodestar/index.js +1 -1
  5. package/lib/api/impl/lodestar/index.js.map +1 -1
  6. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  7. package/lib/chain/blocks/importBlock.js +6 -3
  8. package/lib/chain/blocks/importBlock.js.map +1 -1
  9. package/lib/chain/blocks/importExecutionPayload.d.ts +26 -14
  10. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  11. package/lib/chain/blocks/importExecutionPayload.js +73 -77
  12. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  13. package/lib/chain/blocks/index.d.ts +5 -3
  14. package/lib/chain/blocks/index.d.ts.map +1 -1
  15. package/lib/chain/blocks/index.js +28 -10
  16. package/lib/chain/blocks/index.js.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  18. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  19. package/lib/chain/blocks/types.d.ts +14 -20
  20. package/lib/chain/blocks/types.d.ts.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +81 -12
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/blocks/verifyBlock.d.ts +3 -2
  26. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  27. package/lib/chain/blocks/verifyBlock.js +30 -5
  28. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  29. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  30. package/lib/chain/blocks/verifyBlocksSanityChecks.js +15 -4
  31. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  32. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +24 -0
  33. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -0
  34. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +76 -0
  35. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -0
  36. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +1 -1
  37. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -1
  38. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +2 -11
  39. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -1
  40. package/lib/chain/chain.d.ts +3 -2
  41. package/lib/chain/chain.d.ts.map +1 -1
  42. package/lib/chain/chain.js +14 -3
  43. package/lib/chain/chain.js.map +1 -1
  44. package/lib/chain/errors/blockError.d.ts +8 -1
  45. package/lib/chain/errors/blockError.d.ts.map +1 -1
  46. package/lib/chain/errors/blockError.js +2 -0
  47. package/lib/chain/errors/blockError.js.map +1 -1
  48. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  49. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  50. package/lib/chain/errors/executionPayloadBid.js +1 -0
  51. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  52. package/lib/chain/errors/executionPayloadEnvelope.d.ts +5 -0
  53. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  54. package/lib/chain/errors/executionPayloadEnvelope.js +1 -0
  55. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  56. package/lib/chain/forkChoice/index.js +2 -2
  57. package/lib/chain/forkChoice/index.js.map +1 -1
  58. package/lib/chain/interface.d.ts +3 -2
  59. package/lib/chain/interface.d.ts.map +1 -1
  60. package/lib/chain/interface.js.map +1 -1
  61. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  62. package/lib/chain/prepareNextSlot.js +30 -10
  63. package/lib/chain/prepareNextSlot.js.map +1 -1
  64. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  65. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  66. package/lib/chain/produceBlock/produceBlockBody.js +34 -13
  67. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  68. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +11 -4
  69. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  70. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +20 -18
  71. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  72. package/lib/chain/validation/block.d.ts.map +1 -1
  73. package/lib/chain/validation/block.js +1 -0
  74. package/lib/chain/validation/block.js.map +1 -1
  75. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  76. package/lib/chain/validation/executionPayloadBid.js +13 -1
  77. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  78. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  79. package/lib/chain/validation/executionPayloadEnvelope.js +11 -1
  80. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  81. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  82. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  83. package/lib/metrics/metrics/lodestar.js +4 -0
  84. package/lib/metrics/metrics/lodestar.js.map +1 -1
  85. package/lib/network/processor/gossipHandlers.js +4 -6
  86. package/lib/network/processor/gossipHandlers.js.map +1 -1
  87. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  88. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  89. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  90. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  91. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  92. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  93. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  94. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  95. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  97. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  98. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  99. package/lib/node/notifier.js +7 -1
  100. package/lib/node/notifier.js.map +1 -1
  101. package/lib/sync/range/batch.d.ts +12 -2
  102. package/lib/sync/range/batch.d.ts.map +1 -1
  103. package/lib/sync/range/batch.js +56 -30
  104. package/lib/sync/range/batch.js.map +1 -1
  105. package/lib/sync/range/chain.d.ts +6 -2
  106. package/lib/sync/range/chain.d.ts.map +1 -1
  107. package/lib/sync/range/chain.js +4 -3
  108. package/lib/sync/range/chain.js.map +1 -1
  109. package/lib/sync/range/range.d.ts.map +1 -1
  110. package/lib/sync/range/range.js +17 -6
  111. package/lib/sync/range/range.js.map +1 -1
  112. package/lib/sync/types.d.ts +34 -0
  113. package/lib/sync/types.d.ts.map +1 -1
  114. package/lib/sync/types.js +34 -0
  115. package/lib/sync/types.js.map +1 -1
  116. package/lib/sync/unknownBlock.d.ts +24 -1
  117. package/lib/sync/unknownBlock.d.ts.map +1 -1
  118. package/lib/sync/unknownBlock.js +649 -53
  119. package/lib/sync/unknownBlock.js.map +1 -1
  120. package/lib/sync/utils/downloadByRange.d.ts +46 -10
  121. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  122. package/lib/sync/utils/downloadByRange.js +147 -24
  123. package/lib/sync/utils/downloadByRange.js.map +1 -1
  124. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  125. package/lib/sync/utils/downloadByRoot.js +6 -2
  126. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  127. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  128. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  129. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  130. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  131. package/package.json +16 -15
  132. package/src/api/impl/beacon/blocks/index.ts +5 -2
  133. package/src/api/impl/lodestar/index.ts +1 -1
  134. package/src/chain/blocks/importBlock.ts +4 -2
  135. package/src/chain/blocks/importExecutionPayload.ts +92 -97
  136. package/src/chain/blocks/index.ts +44 -13
  137. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  138. package/src/chain/blocks/types.ts +14 -25
  139. package/src/chain/blocks/utils/chainSegment.ts +106 -17
  140. package/src/chain/blocks/verifyBlock.ts +35 -6
  141. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -7
  142. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +129 -0
  143. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +9 -18
  144. package/src/chain/chain.ts +23 -3
  145. package/src/chain/errors/blockError.ts +4 -1
  146. package/src/chain/errors/executionPayloadBid.ts +6 -0
  147. package/src/chain/errors/executionPayloadEnvelope.ts +6 -0
  148. package/src/chain/forkChoice/index.ts +2 -2
  149. package/src/chain/interface.ts +7 -1
  150. package/src/chain/prepareNextSlot.ts +42 -12
  151. package/src/chain/produceBlock/produceBlockBody.ts +37 -11
  152. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +22 -20
  153. package/src/chain/validation/block.ts +1 -0
  154. package/src/chain/validation/executionPayloadBid.ts +14 -0
  155. package/src/chain/validation/executionPayloadEnvelope.ts +12 -2
  156. package/src/metrics/metrics/lodestar.ts +4 -0
  157. package/src/network/processor/gossipHandlers.ts +6 -6
  158. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  159. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  160. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  161. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  162. package/src/node/notifier.ts +8 -1
  163. package/src/sync/range/batch.ts +90 -35
  164. package/src/sync/range/chain.ts +13 -5
  165. package/src/sync/range/range.ts +18 -6
  166. package/src/sync/types.ts +72 -0
  167. package/src/sync/unknownBlock.ts +810 -57
  168. package/src/sync/utils/downloadByRange.ts +256 -39
  169. package/src/sync/utils/downloadByRoot.ts +12 -2
  170. package/src/sync/utils/pendingBlocksTree.ts +0 -15
@@ -49,7 +49,7 @@ import {
49
49
  ssz,
50
50
  } from "@lodestar/types";
51
51
  import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
52
- import {ZERO_HASH_HEX} from "../../constants/index.js";
52
+ import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js";
53
53
  import {numToQuantity} from "../../execution/engine/utils.js";
54
54
  import {
55
55
  IExecutionBuilder,
@@ -214,9 +214,19 @@ export async function produceBlockBody<T extends BlockType>(
214
214
  });
215
215
 
216
216
  // Get execution payload from EL
217
- const parentBlockHash = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot))
217
+ const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
218
+ let parentBlockHash = isExtendingPayload
218
219
  ? currentState.latestExecutionPayloadBid.blockHash
219
220
  : currentState.latestExecutionPayloadBid.parentBlockHash;
221
+ // At gloas genesis the committed bid has no prior EL block to reference
222
+ // (`bid.parentBlockHash` is zero). Fall back to `bid.blockHash` (= eth1 genesis hash) so the
223
+ // FCU to the EL carries a valid head. Post-genesis bids always reference a non-zero parent.
224
+ if (isStatePostGloas(currentState) && byteArrayEquals(parentBlockHash, ZERO_HASH)) {
225
+ parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
226
+ }
227
+ const parentExecutionRequests = isExtendingPayload
228
+ ? await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot)
229
+ : ssz.electra.ExecutionRequests.defaultValue();
220
230
  const prepareRes = await prepareExecutionPayload(
221
231
  this,
222
232
  this.logger,
@@ -226,7 +236,8 @@ export async function produceBlockBody<T extends BlockType>(
226
236
  safeBlockHash,
227
237
  finalizedBlockHash ?? ZERO_HASH_HEX,
228
238
  currentState,
229
- feeRecipient
239
+ feeRecipient,
240
+ parentExecutionRequests
230
241
  );
231
242
 
232
243
  const {prepType, payloadId} = prepareRes;
@@ -282,6 +293,7 @@ export async function produceBlockBody<T extends BlockType>(
282
293
  gloasBody.signedExecutionPayloadBid = signedBid;
283
294
  // TODO GLOAS: Get payload attestations from pool for previous slot
284
295
  gloasBody.payloadAttestations = [];
296
+ gloasBody.parentExecutionRequests = parentExecutionRequests;
285
297
  blockBody = gloasBody as AssembledBodyType<T>;
286
298
 
287
299
  // Store execution payload data required to construct execution payload envelope later
@@ -618,7 +630,8 @@ export async function prepareExecutionPayload(
618
630
  safeBlockHash: RootHex,
619
631
  finalizedBlockHash: RootHex,
620
632
  state: IBeaconStateViewBellatrix,
621
- suggestedFeeRecipient: string
633
+ suggestedFeeRecipient: string,
634
+ parentExecutionRequests?: electra.ExecutionRequests
622
635
  ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
623
636
  const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
624
637
  const prevRandao = state.getRandaoMix(state.epoch);
@@ -655,6 +668,7 @@ export async function prepareExecutionPayload(
655
668
  parentBlockRoot,
656
669
  parentBlockHash,
657
670
  feeRecipient: suggestedFeeRecipient,
671
+ parentExecutionRequests,
658
672
  });
659
673
 
660
674
  payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
@@ -713,12 +727,14 @@ export function getPayloadAttributesForSSE(
713
727
  parentBlockRoot,
714
728
  parentBlockHash,
715
729
  feeRecipient,
730
+ parentExecutionRequests,
716
731
  }: {
717
732
  prepareState: IBeaconStateViewBellatrix;
718
733
  prepareSlot: Slot;
719
734
  parentBlockRoot: Root;
720
735
  parentBlockHash: Bytes32;
721
736
  feeRecipient: string;
737
+ parentExecutionRequests?: electra.ExecutionRequests;
722
738
  }
723
739
  ): SSEPayloadAttributes {
724
740
  const payloadAttributes = preparePayloadAttributes(fork, chain, {
@@ -727,6 +743,7 @@ export function getPayloadAttributesForSSE(
727
743
  parentBlockRoot,
728
744
  parentBlockHash,
729
745
  feeRecipient,
746
+ parentExecutionRequests,
730
747
  });
731
748
 
732
749
  let parentBlockNumber: number;
@@ -765,12 +782,14 @@ function preparePayloadAttributes(
765
782
  parentBlockRoot,
766
783
  parentBlockHash,
767
784
  feeRecipient,
785
+ parentExecutionRequests,
768
786
  }: {
769
787
  prepareState: IBeaconStateViewBellatrix;
770
788
  prepareSlot: Slot;
771
789
  parentBlockRoot: Root;
772
790
  parentBlockHash: Bytes32;
773
791
  feeRecipient: string;
792
+ parentExecutionRequests?: electra.ExecutionRequests;
774
793
  }
775
794
  ): SSEPayloadAttributes["payloadAttributes"] {
776
795
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
@@ -788,13 +807,20 @@ function preparePayloadAttributes(
788
807
 
789
808
  if (isStatePostGloas(prepareState)) {
790
809
  const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
791
- // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
792
- // already deducted from CL balances but never credited on the EL (the envelope
793
- // was not delivered). The next payload must carry those same withdrawals to
794
- // restore CL/EL consistency, otherwise validators permanently lose that balance.
795
- (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = isExtendingPayload
796
- ? prepareState.getExpectedWithdrawals().expectedWithdrawals
797
- : prepareState.payloadExpectedWithdrawals;
810
+ if (isExtendingPayload) {
811
+ if (parentExecutionRequests === undefined) {
812
+ throw new Error("parentExecutionRequests required when extending full parent");
813
+ }
814
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
815
+ prepareState.getExpectedWithdrawalsForFullParent(parentExecutionRequests);
816
+ } else {
817
+ // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
818
+ // already deducted from CL balances but never credited on the EL (the envelope
819
+ // was not delivered). The next payload must carry those same withdrawals to
820
+ // restore CL/EL consistency, otherwise validators permanently lose that balance.
821
+ (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
822
+ prepareState.payloadExpectedWithdrawals;
823
+ }
798
824
  } else {
799
825
  // withdrawals logic is now fork aware as it changes on electra fork post capella
800
826
  (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
@@ -1,6 +1,6 @@
1
1
  import {CheckpointWithHex} from "@lodestar/fork-choice";
2
2
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
- import {RootHex} from "@lodestar/types";
3
+ import {RootHex, Slot} from "@lodestar/types";
4
4
  import {Logger} from "@lodestar/utils";
5
5
  import {Metrics} from "../../metrics/metrics.js";
6
6
  import {SerializedCache} from "../../util/serializedCache.js";
@@ -21,8 +21,15 @@ export type SeenPayloadEnvelopeInputModules = {
21
21
  /**
22
22
  * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root.
23
23
  *
24
- * Created during block import when a block is processed.
25
- * Pruned on finalization and after payload is written to DB.
24
+ * Created during block import when a block is processed. Two pruning paths:
25
+ * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build
26
+ * on is known.
27
+ * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup.
28
+ *
29
+ * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
30
+ * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
31
+ * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips
32
+ * ticks; subsequent ticks settle it back.
26
33
  */
27
34
  export class SeenPayloadEnvelopeInput {
28
35
  private readonly chainEvents: ChainEventEmitter;
@@ -58,16 +65,7 @@ export class SeenPayloadEnvelopeInput {
58
65
  }
59
66
 
60
67
  private onFinalized = (checkpoint: CheckpointWithHex): void => {
61
- // Prune all entries with slot < finalized slot
62
- const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch);
63
- let deletedCount = 0;
64
- for (const [, input] of this.payloadInputs) {
65
- if (input.slot < finalizedSlot) {
66
- this.evictPayloadInput(input);
67
- deletedCount++;
68
- }
69
- }
70
- this.logger?.debug("SeenPayloadEnvelopeInput.onFinalized deleted cached entries", {deletedCount});
68
+ this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch));
71
69
  };
72
70
 
73
71
  add(props: CreateFromBlockProps): PayloadEnvelopeInput {
@@ -88,17 +86,21 @@ export class SeenPayloadEnvelopeInput {
88
86
  return this.payloadInputs.get(blockRootHex)?.hasPayloadEnvelope() ?? false;
89
87
  }
90
88
 
91
- prune(blockRootHex: RootHex): void {
92
- const payloadInput = this.payloadInputs.get(blockRootHex);
93
- if (payloadInput) {
94
- this.evictPayloadInput(payloadInput);
95
- }
96
- }
97
-
98
89
  size(): number {
99
90
  return this.payloadInputs.size;
100
91
  }
101
92
 
93
+ pruneBelow(slot: Slot): void {
94
+ let deletedCount = 0;
95
+ for (const [, input] of this.payloadInputs) {
96
+ if (input.slot < slot) {
97
+ this.evictPayloadInput(input);
98
+ deletedCount++;
99
+ }
100
+ }
101
+ this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount});
102
+ }
103
+
102
104
  private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void {
103
105
  this.serializedCache.delete(payloadInput.getSerializedCacheKeys());
104
106
  this.payloadInputs.delete(payloadInput.blockRootHex);
@@ -103,6 +103,7 @@ export async function validateGossipBlock(
103
103
  if (chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) {
104
104
  throw new BlockGossipError(GossipAction.IGNORE, {
105
105
  code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
106
+ parentRoot,
106
107
  parentBlockHash: parentBlockHashHex,
107
108
  });
108
109
  }
@@ -1,5 +1,6 @@
1
1
  import {PublicKey} from "@chainsafe/blst";
2
2
  import {
3
+ computeEpochAtSlot,
3
4
  createSingleSignatureSetFromComponents,
4
5
  getExecutionPayloadBidSigningRoot,
5
6
  isActiveBuilder,
@@ -76,6 +77,19 @@ async function validateExecutionPayloadBid(
76
77
  // `SignedProposerPreferences` associated with `bid.slot`.
77
78
  // TODO GLOAS: Implement this along with proposer preference
78
79
 
80
+ // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
81
+ // consensus layer -- i.e. validate that
82
+ // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
83
+ const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
84
+ const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
85
+ if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
86
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
87
+ code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
88
+ blobKzgCommitmentsLen,
89
+ commitmentLimit: maxBlobsPerBlock,
90
+ });
91
+ }
92
+
79
93
  // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot.
80
94
  if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) {
81
95
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
@@ -4,8 +4,8 @@ import {
4
4
  getExecutionPayloadEnvelopeSignatureSet,
5
5
  isStatePostGloas,
6
6
  } from "@lodestar/state-transition";
7
- import {gloas} from "@lodestar/types";
8
- import {toRootHex} from "@lodestar/utils";
7
+ import {gloas, ssz} from "@lodestar/types";
8
+ import {byteArrayEquals, toRootHex} from "@lodestar/utils";
9
9
  import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
11
  import {RegenCaller} from "../regen/index.js";
@@ -107,6 +107,16 @@ async function validateExecutionPayloadEnvelope(
107
107
  });
108
108
  }
109
109
 
110
+ // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root`
111
+ const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests);
112
+ if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) {
113
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
114
+ code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH,
115
+ envelopeRequestsRoot: toRootHex(requestsRoot),
116
+ bidRequestsRoot: toRootHex(payloadInput.getBid().executionRequestsRoot),
117
+ });
118
+ }
119
+
110
120
  // Get the block state to verify the builder's signature.
111
121
  const blockState = await chain.regen
112
122
  .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope)
@@ -613,6 +613,10 @@ export function createLodestarMetrics(
613
613
  name: "lodestar_sync_unknown_block_pending_blocks_size",
614
614
  help: "Current size of UnknownBlockSync pending blocks cache",
615
615
  }),
616
+ pendingPayloads: register.gauge({
617
+ name: "lodestar_sync_unknown_block_pending_payloads_size",
618
+ help: "Current size of UnknownBlockSync pending payloads cache",
619
+ }),
616
620
  knownBadBlocks: register.gauge({
617
621
  name: "lodestar_sync_unknown_block_known_bad_blocks_size",
618
622
  help: "Current size of UnknownBlockSync known bad blocks cache",
@@ -198,7 +198,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
198
198
  } catch (e) {
199
199
  if (e instanceof BlockGossipError) {
200
200
  logger.debug("Gossip block has error", {slot, root: blockShortHex, code: e.type.code});
201
- if (e.type.code === BlockErrorCode.PARENT_UNKNOWN && blockInput) {
201
+ if (
202
+ (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) &&
203
+ blockInput
204
+ ) {
202
205
  chain.emitter.emit(ChainEvent.blockUnknownParent, {
203
206
  blockInput,
204
207
  peer: peerIdStr,
@@ -1057,10 +1060,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1057
1060
  const signedEnvelope = sszDeserialize(topic, serializedData);
1058
1061
  const envelope = signedEnvelope.message;
1059
1062
 
1060
- // TODO GLOAS: consider optimistically create PayloadEnvelopeInput here similar to how we do that for beacon_block
1061
- // so that UnknownBlockSync can handle backward sync
1062
- // the problem now is we cannot create a PayloadEnvelopeInput without the beacon block being known, we need at least the proposer index
1063
- // we can achieve that by looking into the EpochCache
1063
+ // unlike BlockInput, we send the envelope into UnknownBlockInput sync
1064
+ // inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip
1064
1065
  try {
1065
1066
  await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope);
1066
1067
  } catch (e) {
@@ -1069,7 +1070,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1069
1070
  const slot = signedEnvelope.message.payload.slotNumber;
1070
1071
  logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code});
1071
1072
  if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN) {
1072
- // TODO GLOAS: UnknownBlockSync to handle this
1073
1073
  chain.emitter.emit(ChainEvent.envelopeUnknownBlock, {
1074
1074
  envelope: signedEnvelope,
1075
1075
  peer: peerIdStr,
@@ -24,6 +24,10 @@ export async function* onBeaconBlocksByRange(
24
24
  // in the case of initializing from a non-finalized state, we don't have the finalized block so this api does not work
25
25
  // chain.forkChoice.getFinalizeBlock().slot
26
26
  const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot();
27
+ // Blocks are migrated to blockArchive at finalization (including the finalized block itself),
28
+ // so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain loop
29
+ // starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
30
+ const archiveMaxSlot = finalizedSlot;
27
31
 
28
32
  const forkName = chain.config.getForkName(startSlot);
29
33
  if (isForkPostFulu(forkName) && startSlot < chain.earliestAvailableSlot) {
@@ -35,9 +39,12 @@ export async function* onBeaconBlocksByRange(
35
39
  }
36
40
 
37
41
  // Finalized range of blocks
38
- if (startSlot <= finalizedSlot) {
42
+ if (startSlot <= archiveMaxSlot) {
39
43
  // Chain of blobs won't change
40
- for await (const {key, value} of finalized.binaryEntriesStream({gte: startSlot, lt: endSlot})) {
44
+ for await (const {key, value} of finalized.binaryEntriesStream({
45
+ gte: startSlot,
46
+ lt: Math.min(endSlot, archiveMaxSlot + 1),
47
+ })) {
41
48
  yield {
42
49
  data: value,
43
50
  boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(finalized.decodeKey(key))),
@@ -46,19 +53,20 @@ export async function* onBeaconBlocksByRange(
46
53
  }
47
54
 
48
55
  // Non-finalized range of blocks
49
- if (endSlot > finalizedSlot) {
56
+ if (endSlot > archiveMaxSlot) {
50
57
  const headBlock = chain.forkChoice.getHead();
51
58
  const headRoot = headBlock.blockRoot;
52
59
  // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg
53
60
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
54
- // getAllAncestorBlocks response includes the head node, so it's the full chain.
61
+ // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary.
55
62
 
56
63
  // Iterate head chain with ascending block numbers
57
64
  for (let i = headChain.length - 1; i >= 0; i--) {
58
65
  const block = headChain[i];
59
66
 
60
- // Must include only blocks in the range requested
61
- if (block.slot >= startSlot && block.slot < endSlot) {
67
+ // Must include only blocks in the range requested, and skip anything the archive loop
68
+ // above already served via the block.slot > archiveMaxSlot filter.
69
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
62
70
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
63
71
  // at the time of the start of the request. Spec is clear the chain of blobs must be consistent, but on
64
72
  // re-org there's no need to abort the request
@@ -20,31 +20,37 @@ export async function* onBlobSidecarsByRange(
20
20
  const finalized = db.blobSidecarsArchive;
21
21
  const unfinalized = db.blobSidecars;
22
22
  const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot;
23
+ // Blobs are migrated to blobSidecarsArchive at finalization (including the finalized block
24
+ // itself), so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain
25
+ // loop starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic.
26
+ const archiveMaxSlot = finalizedSlot;
23
27
 
24
28
  // Finalized range of blobs
25
- if (startSlot <= finalizedSlot) {
29
+ if (startSlot <= archiveMaxSlot) {
26
30
  // Chain of blobs won't change
27
31
  for await (const {key, value: blobSideCarsBytesWrapped} of finalized.binaryEntriesStream({
28
32
  gte: startSlot,
29
- lt: endSlot,
33
+ lt: Math.min(endSlot, archiveMaxSlot + 1),
30
34
  })) {
31
35
  yield* iterateBlobBytesFromWrapper(chain, blobSideCarsBytesWrapped, finalized.decodeKey(key));
32
36
  }
33
37
  }
34
38
 
35
39
  // Non-finalized range of blobs
36
- if (endSlot > finalizedSlot) {
40
+ if (endSlot > archiveMaxSlot) {
37
41
  const headBlock = chain.forkChoice.getHead();
38
42
  const headRoot = headBlock.blockRoot;
39
43
  // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg
40
44
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
45
+ // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary.
41
46
 
42
47
  // Iterate head chain with ascending block numbers
43
48
  for (let i = headChain.length - 1; i >= 0; i--) {
44
49
  const block = headChain[i];
45
50
 
46
- // Must include only blobs in the range requested
47
- if (block.slot >= startSlot && block.slot < endSlot) {
51
+ // Must include only blobs in the range requested, and skip anything the archive loop
52
+ // above already served via the block.slot > archiveMaxSlot filter.
53
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
48
54
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
49
55
  // at the time of the start of the request. Spec is clear the chain of blobs must be consistent, but on
50
56
  // re-org there's no need to abort the request
@@ -1,6 +1,6 @@
1
1
  import {PeerId} from "@libp2p/interface";
2
2
  import {ChainConfig} from "@lodestar/config";
3
- import {GENESIS_SLOT} from "@lodestar/params";
3
+ import {ForkSeq, GENESIS_SLOT} from "@lodestar/params";
4
4
  import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
5
  import {computeEpochAtSlot} from "@lodestar/state-transition";
6
6
  import {ColumnIndex, Epoch, fulu} from "@lodestar/types";
@@ -43,10 +43,19 @@ export async function* onDataColumnSidecarsByRange(
43
43
 
44
44
  const finalized = db.dataColumnSidecarArchive;
45
45
  const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot;
46
+ // Columns of the last finalized block live in different DBs depending on fork:
47
+ // - Pre-gloas (fulu): migrated to dataColumnSidecarArchive in the same finalization run.
48
+ // - Post-gloas: stay in the hot db (db.dataColumnSidecar) until the next finalization run,
49
+ // because the migration filter requires payloadStatus === FULL for gloas blocks.
50
+ // archiveMaxSlot is the last slot whose columns are served by the archive loop below;
51
+ // anything above it is served by the headChain loop.
52
+ const isPostGloasFinalized = chain.config.getForkSeq(finalizedSlot) >= ForkSeq.gloas;
53
+ const archiveMaxSlot = isPostGloasFinalized ? finalizedSlot - 1 : finalizedSlot;
46
54
 
47
55
  // Finalized range of columns
48
- if (startSlot <= finalizedSlot) {
49
- for (let slot = startSlot; slot < endSlot; slot++) {
56
+ if (startSlot <= archiveMaxSlot) {
57
+ const archiveEnd = Math.min(endSlot, archiveMaxSlot + 1);
58
+ for (let slot = startSlot; slot < archiveEnd; slot++) {
50
59
  const dataColumnSidecars = await finalized.getManyBinary(slot, availableColumns);
51
60
 
52
61
  const unavailableColumnIndices: ColumnIndex[] = [];
@@ -81,9 +90,12 @@ export async function* onDataColumnSidecarsByRange(
81
90
  }
82
91
 
83
92
  // Non-finalized range of columns
84
- if (endSlot > finalizedSlot) {
93
+ if (endSlot > archiveMaxSlot) {
85
94
  const headBlock = chain.forkChoice.getHead();
86
95
  const headRoot = headBlock.blockRoot;
96
+ // getAllAncestorBlocks includes the last finalized block as its final element.
97
+ // Skip anything the archive loop above already served via the block.slot > archiveMaxSlot
98
+ // filter below (pre-gloas this skips finalizedSlot, post-gloas it keeps it).
87
99
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
88
100
 
89
101
  // Iterate head chain with ascending block numbers
@@ -91,7 +103,7 @@ export async function* onDataColumnSidecarsByRange(
91
103
  const block = headChain[i];
92
104
 
93
105
  // Must include only columns in the range requested
94
- if (block.slot >= startSlot && block.slot < endSlot) {
106
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
95
107
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
96
108
  // at the time of the start of the request. Spec is clear the chain of columns must be consistent, but on
97
109
  // re-org there's no need to abort the request
@@ -21,12 +21,15 @@ export async function* onExecutionPayloadEnvelopesByRange(
21
21
 
22
22
  const finalized = db.executionPayloadEnvelopeArchive;
23
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).
26
+ const archiveMaxSlot = finalizedSlot - 1;
24
27
 
25
28
  // Finalized range of envelopes
26
- if (startSlot <= finalizedSlot) {
29
+ if (startSlot <= archiveMaxSlot) {
27
30
  for await (const {key, value: envelopeBytes} of finalized.binaryEntriesStream({
28
31
  gte: startSlot,
29
- lt: endSlot,
32
+ lt: Math.min(endSlot, archiveMaxSlot + 1),
30
33
  })) {
31
34
  const slot = finalized.decodeKey(key);
32
35
  yield {
@@ -37,7 +40,7 @@ export async function* onExecutionPayloadEnvelopesByRange(
37
40
  }
38
41
 
39
42
  // Non-finalized range of envelopes
40
- if (endSlot > finalizedSlot) {
43
+ if (endSlot > archiveMaxSlot) {
41
44
  const headBlock = chain.forkChoice.getHead();
42
45
  const headRoot = headBlock.blockRoot;
43
46
  const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus);
@@ -46,7 +49,7 @@ export async function* onExecutionPayloadEnvelopesByRange(
46
49
  for (let i = headChain.length - 1; i >= 0; i--) {
47
50
  const block = headChain[i];
48
51
 
49
- if (block.slot >= startSlot && block.slot < endSlot) {
52
+ if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
50
53
  // Skip EMPTY blocks
51
54
  if (block.payloadStatus !== PayloadStatus.FULL) {
52
55
  continue;
@@ -167,7 +167,14 @@ function getHeadExecutionInfo(
167
167
  return [];
168
168
  }
169
169
 
170
- const executionStatusStr = headInfo.executionStatus.toLowerCase();
170
+ // A PayloadSeparated head is a gloas beacon block imported before its payload envelope
171
+ // arrives, in that case the exec-block row surfaces the inherited parent anchor (from the
172
+ // bid), which is already validated. Normalize to "valid" to avoid leaking internal
173
+ // fork-choice bookkeeping into the log. Once the payload envelope arrives and the FULL
174
+ // variant becomes head, executionStatus is Valid/Syncing naturally.
175
+ // TODO GLOAS: revisit once optimistic sync is implemented
176
+ const executionStatusStr =
177
+ headInfo.executionStatus === ExecutionStatus.PayloadSeparated ? "valid" : headInfo.executionStatus.toLowerCase();
171
178
 
172
179
  // Add execution status to notifier only if head is on/post bellatrix
173
180
  if (isStatePostBellatrix(headState) && headState.isExecutionStateType) {