@lodestar/beacon-node 1.43.0-dev.87cbe69c66 → 1.43.0-dev.8ad5d8be4c

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 (153) hide show
  1. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  2. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  3. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  4. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  5. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  6. package/lib/chain/blocks/importBlock.js +0 -16
  7. package/lib/chain/blocks/importBlock.js.map +1 -1
  8. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  9. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  10. package/lib/chain/blocks/importExecutionPayload.js +12 -1
  11. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  12. package/lib/chain/blocks/index.d.ts.map +1 -1
  13. package/lib/chain/blocks/index.js +6 -7
  14. package/lib/chain/blocks/index.js.map +1 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
  16. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  17. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
  18. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  19. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  20. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  21. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
  22. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  23. package/lib/chain/blocks/utils/chainSegment.js +29 -26
  24. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  25. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  26. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -4
  27. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  28. package/lib/chain/chain.d.ts.map +1 -1
  29. package/lib/chain/chain.js +25 -10
  30. package/lib/chain/chain.js.map +1 -1
  31. package/lib/chain/initState.d.ts.map +1 -1
  32. package/lib/chain/initState.js +6 -1
  33. package/lib/chain/initState.js.map +1 -1
  34. package/lib/chain/prepareNextSlot.js +1 -1
  35. package/lib/chain/prepareNextSlot.js.map +1 -1
  36. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
  37. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  38. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
  39. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  40. package/lib/chain/seenCache/seenProposerPreferences.d.ts +1 -1
  41. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  42. package/lib/chain/seenCache/seenProposerPreferences.js +6 -11
  43. package/lib/chain/seenCache/seenProposerPreferences.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/gossip/topic.d.ts +749 -2
  49. package/lib/network/gossip/topic.d.ts.map +1 -1
  50. package/lib/network/interface.d.ts +1 -0
  51. package/lib/network/interface.d.ts.map +1 -1
  52. package/lib/network/network.d.ts +1 -0
  53. package/lib/network/network.d.ts.map +1 -1
  54. package/lib/network/network.js +3 -0
  55. package/lib/network/network.js.map +1 -1
  56. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  57. package/lib/network/processor/gossipHandlers.js +45 -1
  58. package/lib/network/processor/gossipHandlers.js.map +1 -1
  59. package/lib/network/reqresp/ReqRespBeaconNode.d.ts.map +1 -1
  60. package/lib/network/reqresp/ReqRespBeaconNode.js +1 -1
  61. package/lib/network/reqresp/ReqRespBeaconNode.js.map +1 -1
  62. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts +9 -0
  63. package/lib/network/reqresp/handlers/beaconBlocksByHead.d.ts.map +1 -0
  64. package/lib/network/reqresp/handlers/beaconBlocksByHead.js +61 -0
  65. package/lib/network/reqresp/handlers/beaconBlocksByHead.js.map +1 -0
  66. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  67. package/lib/network/reqresp/handlers/index.js +5 -0
  68. package/lib/network/reqresp/handlers/index.js.map +1 -1
  69. package/lib/network/reqresp/interface.d.ts +1 -1
  70. package/lib/network/reqresp/interface.js +1 -1
  71. package/lib/network/reqresp/protocols.d.ts +1 -0
  72. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  73. package/lib/network/reqresp/protocols.js +5 -0
  74. package/lib/network/reqresp/protocols.js.map +1 -1
  75. package/lib/network/reqresp/rateLimit.d.ts.map +1 -1
  76. package/lib/network/reqresp/rateLimit.js +4 -0
  77. package/lib/network/reqresp/rateLimit.js.map +1 -1
  78. package/lib/network/reqresp/score.d.ts.map +1 -1
  79. package/lib/network/reqresp/score.js +1 -0
  80. package/lib/network/reqresp/score.js.map +1 -1
  81. package/lib/network/reqresp/types.d.ts +3 -0
  82. package/lib/network/reqresp/types.d.ts.map +1 -1
  83. package/lib/network/reqresp/types.js +3 -0
  84. package/lib/network/reqresp/types.js.map +1 -1
  85. package/lib/sync/constants.d.ts +7 -1
  86. package/lib/sync/constants.d.ts.map +1 -1
  87. package/lib/sync/constants.js +7 -4
  88. package/lib/sync/constants.js.map +1 -1
  89. package/lib/sync/range/batch.d.ts +32 -6
  90. package/lib/sync/range/batch.d.ts.map +1 -1
  91. package/lib/sync/range/batch.js +215 -41
  92. package/lib/sync/range/batch.js.map +1 -1
  93. package/lib/sync/range/chain.d.ts +16 -2
  94. package/lib/sync/range/chain.d.ts.map +1 -1
  95. package/lib/sync/range/chain.js +77 -4
  96. package/lib/sync/range/chain.js.map +1 -1
  97. package/lib/sync/range/range.d.ts.map +1 -1
  98. package/lib/sync/range/range.js +54 -5
  99. package/lib/sync/range/range.js.map +1 -1
  100. package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
  101. package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
  102. package/lib/sync/range/utils/peerBalancer.js +8 -4
  103. package/lib/sync/range/utils/peerBalancer.js.map +1 -1
  104. package/lib/sync/sync.d.ts.map +1 -1
  105. package/lib/sync/sync.js +13 -0
  106. package/lib/sync/sync.js.map +1 -1
  107. package/lib/sync/unknownBlock.d.ts +7 -0
  108. package/lib/sync/unknownBlock.d.ts.map +1 -1
  109. package/lib/sync/unknownBlock.js +127 -10
  110. package/lib/sync/unknownBlock.js.map +1 -1
  111. package/lib/sync/utils/downloadByRange.d.ts +36 -9
  112. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  113. package/lib/sync/utils/downloadByRange.js +89 -42
  114. package/lib/sync/utils/downloadByRange.js.map +1 -1
  115. package/lib/sync/utils/rateLimit.d.ts +2 -0
  116. package/lib/sync/utils/rateLimit.d.ts.map +1 -0
  117. package/lib/sync/utils/rateLimit.js +15 -0
  118. package/lib/sync/utils/rateLimit.js.map +1 -0
  119. package/package.json +16 -16
  120. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  121. package/src/chain/blocks/importBlock.ts +0 -18
  122. package/src/chain/blocks/importExecutionPayload.ts +18 -6
  123. package/src/chain/blocks/index.ts +5 -7
  124. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
  125. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  126. package/src/chain/blocks/utils/chainSegment.ts +30 -27
  127. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -4
  128. package/src/chain/chain.ts +29 -9
  129. package/src/chain/initState.ts +9 -1
  130. package/src/chain/prepareNextSlot.ts +1 -1
  131. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
  132. package/src/chain/seenCache/seenProposerPreferences.ts +7 -12
  133. package/src/metrics/metrics/lodestar.ts +6 -0
  134. package/src/network/interface.ts +1 -0
  135. package/src/network/network.ts +12 -0
  136. package/src/network/processor/gossipHandlers.ts +53 -1
  137. package/src/network/reqresp/ReqRespBeaconNode.ts +1 -0
  138. package/src/network/reqresp/handlers/beaconBlocksByHead.ts +91 -0
  139. package/src/network/reqresp/handlers/index.ts +5 -0
  140. package/src/network/reqresp/interface.ts +1 -1
  141. package/src/network/reqresp/protocols.ts +6 -0
  142. package/src/network/reqresp/rateLimit.ts +4 -0
  143. package/src/network/reqresp/score.ts +1 -0
  144. package/src/network/reqresp/types.ts +5 -0
  145. package/src/sync/constants.ts +8 -4
  146. package/src/sync/range/batch.ts +256 -47
  147. package/src/sync/range/chain.ts +94 -5
  148. package/src/sync/range/range.ts +68 -5
  149. package/src/sync/range/utils/peerBalancer.ts +9 -3
  150. package/src/sync/sync.ts +13 -1
  151. package/src/sync/unknownBlock.ts +157 -10
  152. package/src/sync/utils/downloadByRange.ts +156 -44
  153. package/src/sync/utils/rateLimit.ts +16 -0
@@ -21,6 +21,7 @@ export enum PayloadErrorCode {
21
21
  EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID",
22
22
  EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR",
23
23
  BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE",
24
+ MISS_BLOCK_STATE = "PAYLOAD_ERROR_MISS_BLOCK_STATE",
24
25
  ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
25
26
  INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
26
27
  }
@@ -40,6 +41,10 @@ export type PayloadErrorType =
40
41
  code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE;
41
42
  blockRootHex: string;
42
43
  }
44
+ | {
45
+ code: PayloadErrorCode.MISS_BLOCK_STATE;
46
+ blockRootHex: string;
47
+ }
43
48
  | {
44
49
  code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR;
45
50
  message: string;
@@ -123,12 +128,19 @@ export async function importExecutionPayload(
123
128
  }
124
129
 
125
130
  // 3. Regenerate state for envelope verification
126
- const blockState = await this.regen.getBlockSlotState(
127
- protoBlock,
128
- protoBlock.slot,
129
- {dontTransferCache: true},
130
- RegenCaller.processBlock
131
- );
131
+ const blockState = await this.regen
132
+ .getBlockSlotState(protoBlock, protoBlock.slot, {dontTransferCache: true}, RegenCaller.processBlock)
133
+ .catch(() =>
134
+ // only happen at the 1st batch of skipped slot checkpoint sync
135
+ this.regen.getClosestHeadState(protoBlock)
136
+ );
137
+
138
+ if (blockState == null) {
139
+ throw new PayloadError({
140
+ code: PayloadErrorCode.MISS_BLOCK_STATE,
141
+ blockRootHex: protoBlock.blockRoot,
142
+ });
143
+ }
132
144
  if (!isStatePostGloas(blockState)) {
133
145
  throw new PayloadError({
134
146
  code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
@@ -126,13 +126,11 @@ export async function processBlocks(
126
126
  });
127
127
  }
128
128
 
129
- // Iterate slots from the original `blocks` input (which spans the entire batch including
130
- // slots filtered out of `relevantBlocks`). The first batch of a checkpoint sync may contain
131
- // a payload at the anchor slot whose block is already in fork-choice (added by
132
- // initializeForkChoice as PENDING+EMPTY) and therefore not in verifiedBlocksBySlot — the
133
- // payload still needs to be imported here to populate the anchor's FULL variant so
134
- // subsequent slots can find their parent payload.
135
- const slots = Array.from(new Set(blocks.map((b) => b.getBlock().message.slot)));
129
+ const slotSet = new Set<Slot>(blocks.map((b) => b.getBlock().message.slot));
130
+ if (payloadEnvelopes) {
131
+ for (const slot of payloadEnvelopes.keys()) slotSet.add(slot);
132
+ }
133
+ const slots = Array.from(slotSet).sort((a, b) => a - b);
136
134
  for (const slot of slots) {
137
135
  const fullyVerifiedBlock = verifiedBlocksBySlot.get(slot);
138
136
  if (fullyVerifiedBlock !== undefined) {
@@ -4,7 +4,13 @@ import {toRootHex, withTimeout} from "@lodestar/utils";
4
4
  import {VersionedHashes} from "../../../execution/index.js";
5
5
  import {kzgCommitmentToVersionedHash} from "../../../util/blobs.js";
6
6
  import {MissingColumnMeta} from "../blockInput/types.js";
7
- import {AddPayloadEnvelopeProps, ColumnWithSource, CreateFromBlockProps, SourceMeta} from "./types.js";
7
+ import {
8
+ AddPayloadEnvelopeProps,
9
+ ColumnWithSource,
10
+ CreateFromBidProps,
11
+ CreateFromBlockProps,
12
+ SourceMeta,
13
+ } from "./types.js";
8
14
 
9
15
  export type PayloadEnvelopeInputState =
10
16
  | {
@@ -132,6 +138,26 @@ export class PayloadEnvelopeInput {
132
138
  });
133
139
  }
134
140
 
141
+ /**
142
+ * Create a `PayloadEnvelopeInput` from a state's `latestExecutionPayloadBid` (the bid
143
+ * recorded in beacon state for the latest imported block). Used when seeding the cache
144
+ * for a checkpoint anchor block — we have the bid via state but not the full
145
+ * SignedBeaconBlock body.
146
+ */
147
+ static createFromBid(props: CreateFromBidProps): PayloadEnvelopeInput {
148
+ return new PayloadEnvelopeInput({
149
+ blockRootHex: props.blockRootHex,
150
+ slot: props.slot,
151
+ forkName: props.forkName,
152
+ proposerIndex: props.proposerIndex,
153
+ bid: props.bid,
154
+ sampledColumns: props.sampledColumns,
155
+ custodyColumns: props.custodyColumns,
156
+ timeCreatedSec: props.timeCreatedSec,
157
+ daOutOfRange: props.daOutOfRange,
158
+ });
159
+ }
160
+
135
161
  getBid(): gloas.ExecutionPayloadBid {
136
162
  return this.bid;
137
163
  }
@@ -311,8 +337,11 @@ export class PayloadEnvelopeInput {
311
337
  return this.state.hasAllData;
312
338
  }
313
339
 
340
+ /**
341
+ * Strictly checks missing sampled columns. Does NOT short-circuit on `state.hasAllData`.
342
+ */
314
343
  getMissingSampledColumnMeta(): MissingColumnMeta {
315
- if (this.state.hasAllData) {
344
+ if (this.state.hasComputedAllData) {
316
345
  return {missing: [], versionedHashes: this.versionedHashes};
317
346
  }
318
347
 
@@ -30,6 +30,23 @@ export type CreateFromBlockProps = {
30
30
  daOutOfRange: boolean;
31
31
  };
32
32
 
33
+ /**
34
+ * Used to seed an entry from a state's `latestExecutionPayloadBid` (e.g., when initializing
35
+ * the chain from a checkpoint anchor state — we have the bid via the state but not the
36
+ * full SignedBeaconBlock).
37
+ */
38
+ export type CreateFromBidProps = {
39
+ blockRootHex: RootHex;
40
+ slot: number;
41
+ forkName: ForkName;
42
+ proposerIndex: number;
43
+ bid: gloas.ExecutionPayloadBid;
44
+ sampledColumns: ColumnIndex[];
45
+ custodyColumns: ColumnIndex[];
46
+ timeCreatedSec: number;
47
+ daOutOfRange: boolean;
48
+ };
49
+
33
50
  export type AddPayloadEnvelopeProps = SourceMeta & {
34
51
  envelope: gloas.SignedExecutionPayloadEnvelope;
35
52
  };
@@ -17,8 +17,6 @@ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
17
17
  * Assert this chain segment of blocks is linear with slot numbers and hashes,
18
18
  * and that the provided envelopes are consistent with their respective blocks.
19
19
  *
20
- * Must be called after verifyBlocksSanityChecks so that parentBlock (from forkchoice)
21
- * is available to seed the execution hash chain.
22
20
  *
23
21
  * For each block:
24
22
  * - Verifies parent root + slot linearity
@@ -31,7 +29,7 @@ export function assertLinearChainSegment(
31
29
  config: ChainForkConfig,
32
30
  blocks: IBlockInput[],
33
31
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
34
- parentBlock: ProtoBlock
32
+ parentBlock: ProtoBlock | null
35
33
  ): ChainSegmentResult {
36
34
  const warnings: OrphanedPayloadEnvelope[] = [];
37
35
 
@@ -39,15 +37,16 @@ export function assertLinearChainSegment(
39
37
  // Starts from the known forkchoice parent's execution hash.
40
38
  // - FULL variant (envelope present for slot): advances to envelope.payload.blockHash
41
39
  // - EMPTY variant (no envelope for slot): execution hash is unchanged
42
- // null only for pre-merge parents, which cannot precede gloas blocks.
43
- let currentExecHash: string | null = parentBlock.executionPayloadBlockHash;
40
+ let currentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
44
41
  // Checkpoint sync first batch: parent is the anchor PENDING whose executionPayloadBlockHash
45
42
  // is the inherited parentBlockHash semantic (= grandparent's payload), not its own payload.
46
43
  // If parent's own payload envelope arrives in this batch, advance currentExecHash to that
47
44
  // payload's blockHash so the segment validation sees the true EL chain head.
48
- const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
49
- if (parentPayloadInput?.hasPayloadEnvelope()) {
50
- currentExecHash = parentPayloadInput.getBlockHashHex();
45
+ if (parentBlock !== null) {
46
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
47
+ if (parentPayloadInput?.hasPayloadEnvelope()) {
48
+ currentExecHash = parentPayloadInput.getBlockHashHex();
49
+ }
51
50
  }
52
51
  // Track the execution hash before the last FULL advancement so we can recover
53
52
  // if the next block reveals that envelope was orphaned.
@@ -76,27 +75,31 @@ export function assertLinearChainSegment(
76
75
  }
77
76
  }
78
77
 
79
- if (isGloasBeaconBlock(block.message) && currentExecHash !== null) {
80
- // Verify the bid's parentBlockHash matches the tracked execution hash.
81
- // This ensures the block was built on the correct FULL or EMPTY variant of its parent.
82
- const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
83
- if (bidParentHash !== currentExecHash) {
84
- // Maybe the previous slot's FULL envelope was orphaned try falling back.
85
- // If even prevExecHash doesn't match, the segment is non-linear.
86
- if (bidParentHash !== prevExecHash) {
87
- throw new BlockError(block, {
88
- code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
89
- parentRoot: toRootHex(block.message.parentRoot),
90
- parentBlockHash: bidParentHash,
91
- });
92
- }
93
- if (lastFullSlot !== null && payloadEnvelopes !== null) {
94
- const orphanedInput = payloadEnvelopes.get(lastFullSlot);
95
- if (orphanedInput != null) {
96
- warnings.push({slot: lastFullSlot, payloadEnvelopeInput: orphanedInput});
78
+ if (isGloasBeaconBlock(block.message)) {
79
+ // Bid check fires only when we have a seeded execution hash. With parentBlock=null the
80
+ // chain is seeded mid-segment by the first FULL envelope.
81
+ if (currentExecHash !== null) {
82
+ // Verify the bid's parentBlockHash matches the tracked execution hash.
83
+ // This ensures the block was built on the correct FULL or EMPTY variant of its parent.
84
+ const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
85
+ if (bidParentHash !== currentExecHash) {
86
+ // Maybe the previous slot's FULL envelope was orphaned — try falling back.
87
+ // If even prevExecHash doesn't match, the segment is non-linear.
88
+ if (bidParentHash !== prevExecHash) {
89
+ throw new BlockError(block, {
90
+ code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
91
+ parentRoot: toRootHex(block.message.parentRoot),
92
+ parentBlockHash: bidParentHash,
93
+ });
94
+ }
95
+ if (lastFullSlot !== null && payloadEnvelopes !== null) {
96
+ const orphanedInput = payloadEnvelopes.get(lastFullSlot);
97
+ if (orphanedInput != null) {
98
+ warnings.push({slot: lastFullSlot, payloadEnvelopeInput: orphanedInput});
99
+ }
97
100
  }
101
+ currentExecHash = prevExecHash;
98
102
  }
99
- currentExecHash = prevExecHash;
100
103
  }
101
104
 
102
105
  const payloadInput = payloadEnvelopes?.get(slot) ?? null;
@@ -82,16 +82,19 @@ export function verifyExecutionPayloadEnvelope(
82
82
  }
83
83
  }
84
84
 
85
- // Verify the execution payload is valid
86
- if (payload.slotNumber !== state.slot) {
87
- throw new Error(`Slot mismatch between payload and state payload=${payload.slotNumber} state=${state.slot}`);
85
+ // should not use state.slot, it does not work for skipped slot checkpoint sync
86
+ const blockSlot = state.latestBlockHeader.slot;
87
+ if (payload.slotNumber !== blockSlot) {
88
+ throw new Error(
89
+ `Slot mismatch between payload and latest block header payload=${payload.slotNumber} latestBlockHeader=${blockSlot}`
90
+ );
88
91
  }
89
92
  if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) {
90
93
  throw new Error(
91
94
  `Parent hash mismatch between payload and state payload=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}`
92
95
  );
93
96
  }
94
- const expectedTimestamp = computeTimeAtSlot(config, state.slot, state.genesisTime);
97
+ const expectedTimestamp = computeTimeAtSlot(config, blockSlot, state.genesisTime);
95
98
  if (payload.timestamp !== expectedTimestamp) {
96
99
  throw new Error(
97
100
  `Timestamp mismatch between payload and state payload=${payload.timestamp} state=${expectedTimestamp}`
@@ -23,6 +23,7 @@ import {
23
23
  getEffectiveBalancesFromStateBytes,
24
24
  isStatePostAltair,
25
25
  isStatePostElectra,
26
+ isStatePostGloas,
26
27
  } from "@lodestar/state-transition";
27
28
  import {
28
29
  BeaconBlock,
@@ -335,15 +336,6 @@ export class BeaconChain implements IBeaconChain {
335
336
  metrics,
336
337
  logger,
337
338
  });
338
- this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({
339
- config,
340
- clock,
341
- chainEvents: emitter,
342
- signal,
343
- serializedCache: this.serializedCache,
344
- metrics,
345
- logger,
346
- });
347
339
 
348
340
  this._earliestAvailableSlot = anchorState.slot;
349
341
 
@@ -399,6 +391,7 @@ export class BeaconChain implements IBeaconChain {
399
391
  metrics,
400
392
  logger
401
393
  );
394
+
402
395
  const regen = new QueuedStateRegenerator({
403
396
  config,
404
397
  forkChoice,
@@ -423,6 +416,33 @@ export class BeaconChain implements IBeaconChain {
423
416
  this.payloadEnvelopeProcessor = new PayloadEnvelopeProcessor(this, metrics, signal);
424
417
 
425
418
  this.forkChoice = forkChoice;
419
+
420
+ this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({
421
+ config,
422
+ clock,
423
+ forkChoice,
424
+ chainEvents: emitter,
425
+ signal,
426
+ serializedCache: this.serializedCache,
427
+ metrics,
428
+ logger,
429
+ });
430
+
431
+ const anchorBlockSlot = anchorState.latestBlockHeader.slot;
432
+ if (isStatePostGloas(anchorState) && anchorBlockSlot > 0) {
433
+ const anchorBid = anchorState.latestExecutionPayloadBid;
434
+ this.seenPayloadEnvelopeInputCache.addFromBid({
435
+ blockRootHex: toRootHex(checkpoint.root),
436
+ slot: anchorBlockSlot,
437
+ forkName: anchorState.forkName,
438
+ proposerIndex: anchorState.latestBlockHeader.proposerIndex,
439
+ bid: anchorBid,
440
+ sampledColumns: this.custodyConfig.sampledColumns,
441
+ custodyColumns: this.custodyConfig.custodyColumns,
442
+ timeCreatedSec: Math.floor(Date.now() / 1000),
443
+ });
444
+ }
445
+
426
446
  this.clock = clock;
427
447
  this.regen = regen;
428
448
  this.bls = bls;
@@ -1,7 +1,8 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {ZERO_HASH} from "@lodestar/params";
2
+ import {ForkPostGloas, ForkSeq, ZERO_HASH} from "@lodestar/params";
3
3
  import {
4
4
  BeaconStateAllForks,
5
+ BeaconStateGloas,
5
6
  IBeaconStateView,
6
7
  computeEpochAtSlot,
7
8
  computeStartSlotAtEpoch,
@@ -52,6 +53,13 @@ export function createGenesisBlock(config: ChainForkConfig, genesisState: Beacon
52
53
  const genesisBlock = types.SignedBeaconBlock.defaultValue();
53
54
  const stateRoot = genesisState.hashTreeRoot();
54
55
  genesisBlock.message.stateRoot = stateRoot;
56
+
57
+ if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.gloas) {
58
+ const gloasBlock = genesisBlock as SignedBeaconBlock<ForkPostGloas>;
59
+ const gloasState = genesisState as BeaconStateGloas;
60
+ gloasBlock.message.body.signedExecutionPayloadBid.message = gloasState.latestExecutionPayloadBid.toValue();
61
+ }
62
+
55
63
  return genesisBlock;
56
64
  }
57
65
 
@@ -222,7 +222,7 @@ export class PrepareNextSlotScheduler {
222
222
  // and head.parent (proposer-boost-reorg fallback). Anything older is evicted.
223
223
  const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot);
224
224
  if (updatedHeadParent) {
225
- this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot);
225
+ this.chain.seenPayloadEnvelopeInputCache.pruneBelowParent(updatedHeadParent);
226
226
  }
227
227
  }
228
228
 
@@ -1,13 +1,13 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {CheckpointWithHex} from "@lodestar/fork-choice";
2
+ import {CheckpointWithHex, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
3
3
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
- import {RootHex, Slot} from "@lodestar/types";
4
+ import {RootHex} from "@lodestar/types";
5
5
  import {Logger} from "@lodestar/utils";
6
6
  import {Metrics} from "../../metrics/metrics.js";
7
7
  import {IClock} from "../../util/clock.js";
8
8
  import {SerializedCache} from "../../util/serializedCache.js";
9
9
  import {isDaOutOfRange} from "../blocks/blockInput/index.js";
10
- import {CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js";
10
+ import {CreateFromBidProps, CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js";
11
11
  import {ChainEvent, ChainEventEmitter} from "../emitter.js";
12
12
 
13
13
  export type {PayloadEnvelopeInputState} from "../blocks/payloadEnvelopeInput/index.js";
@@ -16,6 +16,7 @@ export {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js";
16
16
  export type SeenPayloadEnvelopeInputModules = {
17
17
  config: ChainForkConfig;
18
18
  clock: IClock;
19
+ forkChoice: IForkChoice;
19
20
  chainEvents: ChainEventEmitter;
20
21
  signal: AbortSignal;
21
22
  serializedCache: SerializedCache;
@@ -26,11 +27,7 @@ export type SeenPayloadEnvelopeInputModules = {
26
27
  /**
27
28
  * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root.
28
29
  *
29
- * Created during block import when a block is processed. Two pruning paths:
30
- * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build
31
- * on is known.
32
- * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup.
33
- *
30
+ * Created whenever we have a block because it needs block bid.
34
31
  * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
35
32
  * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
36
33
  * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips
@@ -39,6 +36,7 @@ export type SeenPayloadEnvelopeInputModules = {
39
36
  export class SeenPayloadEnvelopeInput {
40
37
  private readonly config: ChainForkConfig;
41
38
  private readonly clock: IClock;
39
+ private readonly forkChoice: IForkChoice;
42
40
  private readonly chainEvents: ChainEventEmitter;
43
41
  private readonly signal: AbortSignal;
44
42
  private readonly serializedCache: SerializedCache;
@@ -46,9 +44,19 @@ export class SeenPayloadEnvelopeInput {
46
44
  private readonly logger?: Logger;
47
45
  private payloadInputs = new Map<RootHex, PayloadEnvelopeInput>();
48
46
 
49
- constructor({config, clock, chainEvents, signal, serializedCache, metrics, logger}: SeenPayloadEnvelopeInputModules) {
47
+ constructor({
48
+ config,
49
+ clock,
50
+ forkChoice,
51
+ chainEvents,
52
+ signal,
53
+ serializedCache,
54
+ metrics,
55
+ logger,
56
+ }: SeenPayloadEnvelopeInputModules) {
50
57
  this.config = config;
51
58
  this.clock = clock;
59
+ this.forkChoice = forkChoice;
52
60
  this.chainEvents = chainEvents;
53
61
  this.signal = signal;
54
62
  this.serializedCache = serializedCache;
@@ -67,14 +75,27 @@ export class SeenPayloadEnvelopeInput {
67
75
  });
68
76
  }
69
77
 
70
- this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.onFinalized);
78
+ this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
71
79
  this.signal.addEventListener("abort", () => {
72
- this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.onFinalized);
80
+ this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
73
81
  });
74
82
  }
75
83
 
76
- private onFinalized = (checkpoint: CheckpointWithHex): void => {
77
- this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch));
84
+ private pruneFinalized = (checkpoint: CheckpointWithHex): void => {
85
+ const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch);
86
+ let deletedCount = 0;
87
+ for (const [, input] of this.payloadInputs) {
88
+ if (input.slot < finalizedSlot) {
89
+ this.evictPayloadInput(input);
90
+ deletedCount++;
91
+ }
92
+ }
93
+
94
+ this.logger?.debug("SeenPayloadEnvelopeInput.pruneFinalized deleted entries", {
95
+ finalizedSlot,
96
+ finalizedRoot: checkpoint.rootHex,
97
+ deletedCount,
98
+ });
78
99
  };
79
100
 
80
101
  add(props: Omit<CreateFromBlockProps, "daOutOfRange">): PayloadEnvelopeInput {
@@ -98,6 +119,27 @@ export class SeenPayloadEnvelopeInput {
98
119
  return input;
99
120
  }
100
121
 
122
+ /**
123
+ * Used at chain initialization to seed the anchor block's PayloadEnvelopeInput from
124
+ * `state.latestExecutionPayloadBid`.
125
+ */
126
+ addFromBid(props: Omit<CreateFromBidProps, "daOutOfRange">): PayloadEnvelopeInput {
127
+ const existing = this.payloadInputs.get(props.blockRootHex);
128
+ if (existing !== undefined) {
129
+ return existing;
130
+ }
131
+ const daOutOfRange = isDaOutOfRange(this.config, props.forkName, props.slot, this.clock.currentEpoch);
132
+ const input = PayloadEnvelopeInput.createFromBid({...props, daOutOfRange});
133
+ this.payloadInputs.set(props.blockRootHex, input);
134
+ this.metrics?.seenCache.payloadEnvelopeInput.created.inc();
135
+ this.logger?.verbose("SeenPayloadEnvelopeInput.addFromBid created new entry", {
136
+ slot: input.slot,
137
+ root: props.blockRootHex,
138
+ daOutOfRange,
139
+ });
140
+ return input;
141
+ }
142
+
101
143
  get(blockRootHex: RootHex): PayloadEnvelopeInput | undefined {
102
144
  return this.payloadInputs.get(blockRootHex);
103
145
  }
@@ -110,15 +152,19 @@ export class SeenPayloadEnvelopeInput {
110
152
  return this.payloadInputs.size;
111
153
  }
112
154
 
113
- pruneBelow(slot: Slot): void {
114
- let deletedCount = 0;
115
- for (const [, input] of this.payloadInputs) {
116
- if (input.slot < slot) {
117
- this.evictPayloadInput(input);
118
- deletedCount++;
155
+ pruneBelowParent(parentBlock: ProtoBlock): void {
156
+ for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
157
+ if (block.slot < parentBlock.slot) {
158
+ const input = this.payloadInputs.get(block.blockRoot);
159
+ if (input) {
160
+ this.evictPayloadInput(input);
161
+ this.logger?.verbose("SeenPayloadEnvelopeInput.pruneBelowParent deleted", {
162
+ slot: block.slot,
163
+ root: block.blockRoot,
164
+ });
165
+ }
119
166
  }
120
167
  }
121
- this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount});
122
168
  }
123
169
 
124
170
  private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void {
@@ -5,16 +5,16 @@ import {MapDef} from "@lodestar/utils";
5
5
  * Tracks signed proposer preferences we've already seen per (dependent_root, proposal_slot, validator_index).
6
6
  */
7
7
  export class SeenProposerPreferences {
8
- private readonly validatorBySlotByDependentRoot = new MapDef<RootHex, Map<Slot, ValidatorIndex>>(
9
- () => new Map<Slot, ValidatorIndex>()
8
+ private readonly validatorByDependentRootBySlot = new MapDef<Slot, Map<RootHex, ValidatorIndex>>(
9
+ () => new Map<RootHex, ValidatorIndex>()
10
10
  );
11
11
 
12
12
  isKnown(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
13
- return this.validatorBySlotByDependentRoot.get(dependentRoot)?.get(proposalSlot) === validatorIndex;
13
+ return this.validatorByDependentRootBySlot.get(proposalSlot)?.get(dependentRoot) === validatorIndex;
14
14
  }
15
15
 
16
16
  add(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
17
- this.validatorBySlotByDependentRoot.getOrDefault(dependentRoot).set(proposalSlot, validatorIndex);
17
+ this.validatorByDependentRootBySlot.getOrDefault(proposalSlot).set(dependentRoot, validatorIndex);
18
18
  }
19
19
 
20
20
  /**
@@ -23,14 +23,9 @@ export class SeenProposerPreferences {
23
23
  * on each slot tick.
24
24
  */
25
25
  prune(currentSlot: Slot): void {
26
- for (const [dependentRoot, slotMap] of this.validatorBySlotByDependentRoot.entries()) {
27
- for (const slot of slotMap.keys()) {
28
- if (slot < currentSlot) {
29
- slotMap.delete(slot);
30
- }
31
- }
32
- if (slotMap.size === 0) {
33
- this.validatorBySlotByDependentRoot.delete(dependentRoot);
26
+ for (const slot of this.validatorByDependentRootBySlot.keys()) {
27
+ if (slot < currentSlot) {
28
+ this.validatorByDependentRootBySlot.delete(slot);
34
29
  }
35
30
  }
36
31
  }
@@ -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: {
@@ -78,6 +78,7 @@ export interface INetwork extends INetworkCorePublic {
78
78
  // ReqResp
79
79
  sendBeaconBlocksByRange(peerId: PeerIdStr, request: phase0.BeaconBlocksByRangeRequest): Promise<SignedBeaconBlock[]>;
80
80
  sendBeaconBlocksByRoot(peerId: PeerIdStr, request: BeaconBlocksByRootRequest): Promise<SignedBeaconBlock[]>;
81
+ sendBeaconBlocksByHead(peerId: PeerIdStr, request: fulu.BeaconBlocksByHeadRequest): Promise<SignedBeaconBlock[]>;
81
82
  sendBlobSidecarsByRange(peerId: PeerIdStr, request: deneb.BlobSidecarsByRangeRequest): Promise<deneb.BlobSidecar[]>;
82
83
  sendBlobSidecarsByRoot(peerId: PeerIdStr, request: BlobSidecarsByRootRequest): Promise<deneb.BlobSidecar[]>;
83
84
  sendDataColumnSidecarsByRange(
@@ -578,6 +578,18 @@ export class Network implements INetwork {
578
578
  );
579
579
  }
580
580
 
581
+ async sendBeaconBlocksByHead(
582
+ peerId: PeerIdStr,
583
+ request: fulu.BeaconBlocksByHeadRequest
584
+ ): Promise<SignedBeaconBlock[]> {
585
+ return collectMaxResponseTypedWithBytes(
586
+ this.sendReqRespRequest(peerId, ReqRespMethod.BeaconBlocksByHead, [Version.V1], request),
587
+ Math.min(request.count, this.config.MAX_REQUEST_BLOCKS_DENEB),
588
+ responseSszTypeByMethod[ReqRespMethod.BeaconBlocksByHead],
589
+ this.chain.serializedCache
590
+ );
591
+ }
592
+
581
593
  async sendLightClientBootstrap(peerId: PeerIdStr, request: Root): Promise<LightClientBootstrap> {
582
594
  return collectExactOneTyped(
583
595
  this.sendReqRespRequest(peerId, ReqRespMethod.LightClientBootstrap, [Version.V1], request),