@lodestar/beacon-node 1.43.0-dev.3bcc6d0ad5 → 1.43.0-dev.3fe3b04cbd

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 (123) hide show
  1. package/lib/api/impl/debug/index.d.ts.map +1 -1
  2. package/lib/api/impl/debug/index.js +0 -1
  3. package/lib/api/impl/debug/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +2 -1
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  8. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  9. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  10. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +14 -18
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  15. package/lib/chain/blocks/importExecutionPayload.js +3 -5
  16. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  17. package/lib/chain/blocks/index.d.ts.map +1 -1
  18. package/lib/chain/blocks/index.js +30 -17
  19. package/lib/chain/blocks/index.js.map +1 -1
  20. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +3 -0
  21. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  22. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +4 -1
  23. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  24. package/lib/chain/blocks/types.d.ts +2 -1
  25. package/lib/chain/blocks/types.d.ts.map +1 -1
  26. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  27. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  28. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  29. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  30. package/lib/chain/blocks/verifyBlock.js +5 -6
  31. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  32. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  33. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  34. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  35. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  36. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  37. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  38. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  39. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  40. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  41. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  42. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -2
  43. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  44. package/lib/chain/chain.d.ts.map +1 -1
  45. package/lib/chain/chain.js +10 -9
  46. package/lib/chain/chain.js.map +1 -1
  47. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  48. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  49. package/lib/chain/errors/proposerPreferences.js +1 -0
  50. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  51. package/lib/chain/prepareNextSlot.js +1 -1
  52. package/lib/chain/prepareNextSlot.js.map +1 -1
  53. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  54. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  55. package/lib/chain/produceBlock/produceBlockBody.js +1 -0
  56. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  57. package/lib/chain/regen/queued.d.ts.map +1 -1
  58. package/lib/chain/regen/queued.js +1 -4
  59. package/lib/chain/regen/queued.js.map +1 -1
  60. package/lib/chain/regen/regen.d.ts.map +1 -1
  61. package/lib/chain/regen/regen.js +1 -4
  62. package/lib/chain/regen/regen.js.map +1 -1
  63. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -4
  64. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  65. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +30 -12
  66. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  67. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  68. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  69. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  70. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  71. package/lib/chain/validation/executionPayloadBid.js +11 -8
  72. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  73. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  74. package/lib/chain/validation/proposerPreferences.js +39 -17
  75. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  76. package/lib/network/gossip/topic.d.ts +2 -0
  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 +14 -0
  80. package/lib/network/processor/gossipHandlers.js.map +1 -1
  81. package/lib/node/nodejs.js +2 -2
  82. package/lib/node/nodejs.js.map +1 -1
  83. package/lib/node/notifier.js +1 -7
  84. package/lib/node/notifier.js.map +1 -1
  85. package/lib/sync/range/batch.d.ts.map +1 -1
  86. package/lib/sync/range/batch.js +54 -17
  87. package/lib/sync/range/batch.js.map +1 -1
  88. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  89. package/lib/sync/utils/downloadByRange.js +17 -8
  90. package/lib/sync/utils/downloadByRange.js.map +1 -1
  91. package/lib/util/sszBytes.d.ts.map +1 -1
  92. package/lib/util/sszBytes.js +8 -6
  93. package/lib/util/sszBytes.js.map +1 -1
  94. package/package.json +15 -15
  95. package/src/api/impl/debug/index.ts +0 -1
  96. package/src/api/impl/validator/index.ts +2 -1
  97. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  98. package/src/chain/blocks/importBlock.ts +14 -21
  99. package/src/chain/blocks/importExecutionPayload.ts +3 -5
  100. package/src/chain/blocks/index.ts +20 -9
  101. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +4 -1
  102. package/src/chain/blocks/types.ts +2 -1
  103. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  104. package/src/chain/blocks/verifyBlock.ts +7 -5
  105. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  106. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  107. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -2
  108. package/src/chain/chain.ts +12 -9
  109. package/src/chain/errors/proposerPreferences.ts +9 -1
  110. package/src/chain/prepareNextSlot.ts +1 -1
  111. package/src/chain/produceBlock/produceBlockBody.ts +2 -0
  112. package/src/chain/regen/queued.ts +2 -7
  113. package/src/chain/regen/regen.ts +2 -7
  114. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +43 -14
  115. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  116. package/src/chain/validation/executionPayloadBid.ts +11 -8
  117. package/src/chain/validation/proposerPreferences.ts +37 -18
  118. package/src/network/processor/gossipHandlers.ts +18 -0
  119. package/src/node/nodejs.ts +2 -2
  120. package/src/node/notifier.ts +1 -8
  121. package/src/sync/range/batch.ts +54 -19
  122. package/src/sync/utils/downloadByRange.ts +18 -8
  123. package/src/util/sszBytes.ts +8 -6
@@ -895,8 +895,11 @@ export class BlockInputColumns extends AbstractBlockInput<ForkColumnsDA, fulu.Da
895
895
  return this.getAllColumnsWithSource().map(({columnSidecar}) => columnSidecar);
896
896
  }
897
897
 
898
+ /**
899
+ * Strictly checks missing sampled columns. Does NOT short-circuit on `state.hasAllData`.
900
+ */
898
901
  getMissingSampledColumnMeta(): MissingColumnMeta {
899
- if (this.state.hasAllData) {
902
+ if (this.state.hasComputedAllData) {
900
903
  return {
901
904
  missing: [],
902
905
  versionedHashes: this.state.versionedHashes,
@@ -69,8 +69,8 @@ export async function importBlock(
69
69
  fullyVerifiedBlock: FullyVerifiedBlock,
70
70
  opts: ImportBlockOpts
71
71
  ): Promise<void> {
72
- const {blockInput, postState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} =
73
- fullyVerifiedBlock;
72
+ const {blockInput, postState, parentBlockSlot, dataAvailabilityStatus, indexedAttestations} = fullyVerifiedBlock;
73
+ let {executionStatus} = fullyVerifiedBlock;
74
74
  const block = blockInput.getBlock();
75
75
  const source = blockInput.getBlockSource();
76
76
  const {slot: blockSlot} = block.message;
@@ -105,12 +105,23 @@ export async function importBlock(
105
105
 
106
106
  // Should compute checkpoint balances before forkchoice.onBlock
107
107
  this.checkpointBalancesCache.processState(blockRootHex, postState);
108
+ if (fork >= ForkSeq.gloas) {
109
+ const parentRootHex = toRootHex(block.message.parentRoot);
110
+ const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRootHex);
111
+ if (parentBlock === null) {
112
+ throw Error(`Parent block not found in forkChoice, parentRoot=${parentRootHex}`);
113
+ }
114
+ if (parentBlock.executionStatus === ExecutionStatus.Invalid) {
115
+ throw Error(`Parent block has invalid execution status, parentRoot=${parentRootHex}`);
116
+ }
117
+ executionStatus = parentBlock.executionStatus;
118
+ }
108
119
  const blockSummary = this.forkChoice.onBlock(
109
120
  block.message,
110
121
  postState,
111
122
  blockDelaySec,
112
123
  currentSlot,
113
- fork >= ForkSeq.gloas ? ExecutionStatus.PayloadSeparated : executionStatus,
124
+ executionStatus,
114
125
  dataAvailabilityStatus
115
126
  );
116
127
 
@@ -118,24 +129,6 @@ export async function importBlock(
118
129
  // Some block event handlers require state being in state cache so need to do this before emitting EventType.block
119
130
  this.regen.processState(blockRootHex, postState);
120
131
 
121
- // For range sync we skip triggerGetBlobs because column fetching is handled in the range path.
122
- if (fork >= ForkSeq.gloas && !opts.fromRangeSync) {
123
- const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex);
124
- // PayloadEnvelopeInput is supposed to have right after we have block
125
- // there are 4 sources of them: gossip, by root, by range and api
126
- if (!payloadInput) {
127
- throw Error(`PayloadEnvelopeInput not seeded for block ${blockRootHex} before importBlock`);
128
- }
129
-
130
- // Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments
131
- // which is all the information we need so there is no reason to delay until execution payload arrives
132
- // TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here
133
- // (for example later in the slot). Do not couple retries to incoming gossip columns.
134
- // Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any
135
- // in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path.
136
- this.getBlobsTracker.triggerGetBlobs(payloadInput);
137
- }
138
-
139
132
  this.metrics?.importBlock.bySource.inc({source: source.source});
140
133
  this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex});
141
134
 
@@ -1,7 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ExecutionStatus, PayloadExecutionStatus, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
3
3
  import {DataAvailabilityStatus, isStatePostGloas} from "@lodestar/state-transition";
4
- import {fromHex, isErrorAborted} from "@lodestar/utils";
4
+ import {isErrorAborted} from "@lodestar/utils";
5
5
  import {ZERO_HASH_HEX} from "../../constants/index.js";
6
6
  import {ExecutionPayloadStatus} from "../../execution/index.js";
7
7
  import {isQueueErrorAborted} from "../../util/queue/index.js";
@@ -61,7 +61,6 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
61
61
  switch (status) {
62
62
  case ExecutionPayloadStatus.VALID:
63
63
  return ExecutionStatus.Valid;
64
- // TODO GLOAS: Handle optimistic import for payload
65
64
  case ExecutionPayloadStatus.SYNCING:
66
65
  case ExecutionPayloadStatus.ACCEPTED:
67
66
  return ExecutionStatus.Syncing;
@@ -160,7 +159,7 @@ export async function importExecutionPayload(
160
159
  fork,
161
160
  envelope.payload,
162
161
  payloadInput.getVersionedHashes(),
163
- fromHex(protoBlock.parentRoot),
162
+ envelope.parentBeaconBlockRoot,
164
163
  envelope.executionRequests
165
164
  ),
166
165
 
@@ -255,8 +254,7 @@ export async function importExecutionPayload(
255
254
  builderIndex: envelope.builderIndex,
256
255
  blockHash: blockHashHex,
257
256
  blockRoot: blockRootHex,
258
- // TODO GLOAS: revisit once we support optimistic import
259
- executionOptimistic: false,
257
+ executionOptimistic: execStatus === ExecutionStatus.Syncing,
260
258
  });
261
259
  }
262
260
 
@@ -65,7 +65,7 @@ export async function processBlocks(
65
65
  }
66
66
 
67
67
  try {
68
- const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, opts);
68
+ const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, payloadEnvelopes, opts);
69
69
 
70
70
  // No relevant blocks, skip verifyBlocksInEpoch()
71
71
  if (relevantBlocks.length === 0 || parentBlock === null) {
@@ -109,8 +109,10 @@ export async function processBlocks(
109
109
  }
110
110
 
111
111
  const {executionStatuses} = segmentExecStatus;
112
- const fullyVerifiedBlocks = relevantBlocks.map(
113
- (block, i): FullyVerifiedBlock => ({
112
+ const verifiedBlocksBySlot = new Map<Slot, FullyVerifiedBlock>();
113
+ for (let i = 0; i < relevantBlocks.length; i++) {
114
+ const block = relevantBlocks[i];
115
+ verifiedBlocksBySlot.set(block.getBlock().message.slot, {
114
116
  blockInput: block,
115
117
  postState: postStates[i],
116
118
  parentBlockSlot: parentSlots[i],
@@ -121,14 +123,23 @@ export async function processBlocks(
121
123
  indexedAttestations: indexedAttestationsByBlock[i],
122
124
  // TODO: Make this param mandatory and capture in gossip
123
125
  seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
124
- })
125
- );
126
+ });
127
+ }
126
128
 
127
- for (const fullyVerifiedBlock of fullyVerifiedBlocks) {
128
- // TODO: Consider batching importBlock too if it takes significant time
129
- await importBlock.call(this, fullyVerifiedBlock, opts);
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)));
136
+ for (const slot of slots) {
137
+ const fullyVerifiedBlock = verifiedBlocksBySlot.get(slot);
138
+ if (fullyVerifiedBlock !== undefined) {
139
+ // TODO: Consider batching importBlock too if it takes significant time
140
+ await importBlock.call(this, fullyVerifiedBlock, opts);
141
+ }
130
142
 
131
- const slot = fullyVerifiedBlock.blockInput.getBlock().message.slot;
132
143
  const payloadInput = payloadEnvelopes?.get(slot);
133
144
  if (payloadInput?.hasPayloadEnvelope()) {
134
145
  if (!payloadInput.isComplete()) {
@@ -311,8 +311,11 @@ export class PayloadEnvelopeInput {
311
311
  return this.state.hasAllData;
312
312
  }
313
313
 
314
+ /**
315
+ * Strictly checks missing sampled columns. Does NOT short-circuit on `state.hasAllData`.
316
+ */
314
317
  getMissingSampledColumnMeta(): MissingColumnMeta {
315
- if (this.state.hasAllData) {
318
+ if (this.state.hasComputedAllData) {
316
319
  return {missing: [], versionedHashes: this.versionedHashes};
317
320
  }
318
321
 
@@ -94,7 +94,8 @@ export type ImportBlockOpts = {
94
94
  *
95
95
  * `executionStatus` reflects the outcome of execution payload verification at block-import time:
96
96
  * - pre-gloas: Valid | Syncing | PreMerge (from EL notifyNewPayload against the in-block payload)
97
- * - post-gloas: PayloadSeparated (payload arrives separately as an envelope and is imported later)
97
+ * - post-gloas: inherited from parent's chain (Valid/Syncing) by importBlock; payload arrives
98
+ * separately as an envelope and creates the FULL variant later via onExecutionPayload
98
99
  */
99
100
  export type FullyVerifiedBlock = {
100
101
  blockInput: IBlockInput;
@@ -41,6 +41,14 @@ export function assertLinearChainSegment(
41
41
  // - EMPTY variant (no envelope for slot): execution hash is unchanged
42
42
  // null only for pre-merge parents, which cannot precede gloas blocks.
43
43
  let currentExecHash: string | null = parentBlock.executionPayloadBlockHash;
44
+ // Checkpoint sync first batch: parent is the anchor PENDING whose executionPayloadBlockHash
45
+ // is the inherited parentBlockHash semantic (= grandparent's payload), not its own payload.
46
+ // If parent's own payload envelope arrives in this batch, advance currentExecHash to that
47
+ // 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();
51
+ }
44
52
  // Track the execution hash before the last FULL advancement so we can recover
45
53
  // if the next block reveals that envelope was orphaned.
46
54
  let prevExecHash: string | null = currentExecHash;
@@ -125,15 +125,17 @@ export async function verifyBlocksInEpoch(
125
125
  }> =
126
126
  fork >= ForkSeq.gloas
127
127
  ? (async () => {
128
- const payloadInputsForDa: PayloadEnvelopeInput[] = [];
129
- for (const input of blockInputs) {
130
- const pi = payloadEnvelopes?.get(input.slot);
131
- if (pi !== undefined) payloadInputsForDa.push(pi);
132
- }
128
+ // Validate DA for ALL payloads in the Map, not just those paired with blockInputs.
129
+ // A checkpoint-sync batch may include a payload for a slot whose block was filtered
130
+ // out of relevantBlocks (e.g., the anchor at the finalized slot); that payload still
131
+ // needs DA validation so it can be imported in processBlocks.
132
+ const payloadInputsForDa: PayloadEnvelopeInput[] =
133
+ payloadEnvelopes !== null ? Array.from(payloadEnvelopes.values()) : [];
133
134
  const {dataAvailabilityStatuses, availableTime} = await verifyPayloadsDataAvailability(
134
135
  payloadInputsForDa,
135
136
  abortController.signal
136
137
  );
138
+
137
139
  const payloadDAStatuses = new Map<Slot, DataAvailabilityStatus>();
138
140
  for (let i = 0; i < payloadInputsForDa.length; i++) {
139
141
  payloadDAStatuses.set(payloadInputsForDa[i].slot, dataAvailabilityStatuses[i]);
@@ -46,8 +46,7 @@ type VerifyBlockExecutionResponse =
46
46
  | VerifyExecutionErrorResponse
47
47
  | {executionStatus: ExecutionStatus.Valid; lvhResponse: LVHValidResponse; execError: null}
48
48
  | {executionStatus: ExecutionStatus.Syncing; lvhResponse?: LVHValidResponse; execError: null}
49
- | {executionStatus: ExecutionStatus.PreMerge; lvhResponse: undefined; execError: null}
50
- | {executionStatus: ExecutionStatus.PayloadSeparated; lvhResponse: undefined; execError: null};
49
+ | {executionStatus: ExecutionStatus.PreMerge; lvhResponse: undefined; execError: null};
51
50
 
52
51
  /**
53
52
  * Verifies 1 or more execution payloads from a linear sequence of blocks.
@@ -145,9 +144,10 @@ export async function verifyBlockExecutionPayload(
145
144
  ): Promise<VerifyBlockExecutionResponse> {
146
145
  const block = blockInput.getBlock();
147
146
 
148
- // Gloas block doesn't have execution payload. Return right away
147
+ // Gloas block doesn't have execution payload. Return Syncing as a placeholder; the actual
148
+ // status for gloas PENDING/EMPTY is derived from parent's chain in importBlock.
149
149
  if (isBlockInputNoData(blockInput)) {
150
- return {executionStatus: ExecutionStatus.PayloadSeparated, lvhResponse: undefined, execError: null};
150
+ return {executionStatus: ExecutionStatus.Syncing, lvhResponse: undefined, execError: null};
151
151
  }
152
152
 
153
153
  /** Not null if execution is enabled */
@@ -198,6 +198,7 @@ export async function verifyBlockExecutionPayload(
198
198
  executionStatus,
199
199
  latestValidExecHash: execResult.latestValidHash,
200
200
  invalidateFromParentBlockRoot: blockInput.parentRootHex,
201
+ invalidateFromParentBlockHash: toRootHex(executionPayloadEnabled.parentHash),
201
202
  };
202
203
  const execError = new BlockError(block, {
203
204
  code: BlockErrorCode.EXECUTION_ENGINE_ERROR,
@@ -281,6 +282,7 @@ function getSegmentErrorResponse(
281
282
  executionStatus: ExecutionStatus.Invalid,
282
283
  latestValidExecHash: lvhResponse.latestValidExecHash,
283
284
  invalidateFromParentBlockRoot: parentBlock.blockRoot,
285
+ invalidateFromParentBlockHash: parentBlock.executionPayloadBlockHash,
284
286
  };
285
287
  }
286
288
  }
@@ -7,6 +7,7 @@ import {IClock} from "../../util/clock.js";
7
7
  import {BlockError, BlockErrorCode} from "../errors/index.js";
8
8
  import {IChainOptions} from "../options.js";
9
9
  import {IBlockInput} from "./blockInput/types.js";
10
+ import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
10
11
  import {ImportBlockOpts} from "./types.js";
11
12
 
12
13
  /**
@@ -30,6 +31,7 @@ export function verifyBlocksSanityChecks(
30
31
  blacklistedBlocks: Map<RootHex, Slot | null>;
31
32
  },
32
33
  blocks: IBlockInput[],
34
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
33
35
  opts: ImportBlockOpts
34
36
  ): {
35
37
  relevantBlocks: IBlockInput[];
@@ -100,13 +102,21 @@ export function verifyBlocksSanityChecks(
100
102
  const parentBlockHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
101
103
  const parentBlockWithPayload = chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash);
102
104
  if (!parentBlockWithPayload) {
103
- throw new BlockError(block, {
104
- code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
105
- parentRoot,
106
- parentBlockHash,
107
- });
105
+ // Checkpoint sync: parent's FULL variant may not be in fork-choice yet because the
106
+ // anchor block is initialized with PENDING+EMPTY only. The parent's payload arrives
107
+ // in the same batch via payloadEnvelopes and will be imported by processBlocks. If
108
+ // a matching payload is in the Map, accept the parent as known.
109
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlockDefaultStatus.slot);
110
+ if (parentPayloadInput?.getBlockHashHex() !== parentBlockHash) {
111
+ throw new BlockError(block, {
112
+ code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
113
+ parentRoot,
114
+ parentBlockHash,
115
+ });
116
+ }
117
+ } else {
118
+ parentBlock = parentBlockWithPayload;
108
119
  }
109
- parentBlock = parentBlockWithPayload;
110
120
  }
111
121
  // Parent is known to the fork-choice
112
122
  parentBlockSlot = parentBlock.slot;
@@ -20,7 +20,7 @@ export type VerifyExecutionPayloadEnvelopeOpts = {
20
20
  * performed outside this function, see `verifyExecutionPayloadEnvelopeSignature` and
21
21
  * `importExecutionPayload` which run both in parallel with this check.
22
22
  *
23
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope
23
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope
24
24
  */
25
25
  export function verifyExecutionPayloadEnvelope(
26
26
  config: BeaconConfig,
@@ -43,6 +43,11 @@ export function verifyExecutionPayloadEnvelope(
43
43
  `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}`
44
44
  );
45
45
  }
46
+ if (!byteArrayEquals(envelope.parentBeaconBlockRoot, state.latestBlockHeader.parentRoot)) {
47
+ throw new Error(
48
+ `Envelope's parent_beacon_block_root mismatch envelope=${toRootHex(envelope.parentBeaconBlockRoot)} state=${toRootHex(state.latestBlockHeader.parentRoot)}`
49
+ );
50
+ }
46
51
 
47
52
  // Verify consistency with the committed bid
48
53
  const bid = state.latestExecutionPayloadBid;
@@ -108,7 +113,7 @@ export function verifyExecutionPayloadEnvelope(
108
113
  /**
109
114
  * Verify the BLS signature of an execution payload envelope.
110
115
  *
111
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature
116
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature
112
117
  */
113
118
  export async function verifyExecutionPayloadEnvelopeSignature(
114
119
  config: BeaconConfig,
@@ -335,15 +335,6 @@ export class BeaconChain implements IBeaconChain {
335
335
  metrics,
336
336
  logger,
337
337
  });
338
- this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({
339
- config,
340
- clock,
341
- chainEvents: emitter,
342
- signal,
343
- serializedCache: this.serializedCache,
344
- metrics,
345
- logger,
346
- });
347
338
 
348
339
  this._earliestAvailableSlot = anchorState.slot;
349
340
 
@@ -423,6 +414,18 @@ export class BeaconChain implements IBeaconChain {
423
414
  this.payloadEnvelopeProcessor = new PayloadEnvelopeProcessor(this, metrics, signal);
424
415
 
425
416
  this.forkChoice = forkChoice;
417
+
418
+ this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({
419
+ config,
420
+ clock,
421
+ forkChoice,
422
+ chainEvents: emitter,
423
+ signal,
424
+ serializedCache: this.serializedCache,
425
+ metrics,
426
+ logger,
427
+ });
428
+
426
429
  this.clock = clock;
427
430
  this.regen = regen;
428
431
  this.bls = bls;
@@ -1,9 +1,10 @@
1
- import {Slot, ValidatorIndex} from "@lodestar/types";
1
+ import {RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
2
  import {GossipActionError} from "./gossipValidation.js";
3
3
 
4
4
  export enum ProposerPreferencesErrorCode {
5
5
  INVALID_EPOCH = "PROPOSER_PREFERENCES_ERROR_INVALID_EPOCH",
6
6
  PROPOSAL_SLOT_PASSED = "PROPOSER_PREFERENCES_ERROR_PROPOSAL_SLOT_PASSED",
7
+ UNKNOWN_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_UNKNOWN_DEPENDENT_ROOT",
7
8
  INVALID_PROPOSER = "PROPOSER_PREFERENCES_ERROR_INVALID_PROPOSER",
8
9
  ALREADY_KNOWN = "PROPOSER_PREFERENCES_ERROR_ALREADY_KNOWN",
9
10
  INVALID_SIGNATURE = "PROPOSER_PREFERENCES_ERROR_INVALID_SIGNATURE",
@@ -20,15 +21,22 @@ export type ProposerPreferencesErrorType =
20
21
  proposalSlot: Slot;
21
22
  currentSlot: Slot;
22
23
  }
24
+ | {
25
+ code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT;
26
+ proposalSlot: Slot;
27
+ dependentRoot: RootHex;
28
+ }
23
29
  | {
24
30
  code: ProposerPreferencesErrorCode.INVALID_PROPOSER;
25
31
  proposalSlot: Slot;
26
32
  validatorIndex: ValidatorIndex;
33
+ dependentRoot: RootHex;
27
34
  }
28
35
  | {
29
36
  code: ProposerPreferencesErrorCode.ALREADY_KNOWN;
30
37
  proposalSlot: Slot;
31
38
  validatorIndex: ValidatorIndex;
39
+ dependentRoot: RootHex;
32
40
  }
33
41
  | {
34
42
  code: ProposerPreferencesErrorCode.INVALID_SIGNATURE;
@@ -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
 
@@ -111,6 +111,7 @@ export type ProduceFullGloas = {
111
111
  executionRequests: electra.ExecutionRequests;
112
112
  blobsBundle: BlobsBundle<ForkPostGloas>;
113
113
  cells: fulu.Cell[][];
114
+ parentBlockRoot: Root;
114
115
  };
115
116
  export type ProduceFullFulu = {
116
117
  type: BlockType.Full;
@@ -311,6 +312,7 @@ export async function produceBlockBody<T extends BlockType>(
311
312
  gloasResult.executionRequests = executionRequests;
312
313
  gloasResult.blobsBundle = blobsBundle;
313
314
  gloasResult.cells = cells;
315
+ gloasResult.parentBlockRoot = fromHex(parentBlock.blockRoot);
314
316
 
315
317
  const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
316
318
  this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
@@ -1,7 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
3
3
  import {IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
4
- import {BeaconBlock, Epoch, RootHex, Slot, isGloasBeaconBlock, phase0} from "@lodestar/types";
4
+ import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types";
5
5
  import {Logger, toRootHex} from "@lodestar/utils";
6
6
  import {Metrics} from "../../metrics/index.js";
7
7
  import {JobItemQueue} from "../../util/queue/index.js";
@@ -88,12 +88,7 @@ export class QueuedStateRegenerator implements IStateRegenerator {
88
88
  */
89
89
  getPreStateSync(block: BeaconBlock): IBeaconStateView | null {
90
90
  const parentRoot = toRootHex(block.parentRoot);
91
- const parentBlock = isGloasBeaconBlock(block)
92
- ? this.forkChoice.getBlockHexAndBlockHash(
93
- parentRoot,
94
- toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
95
- )
96
- : this.forkChoice.getBlockHexDefaultStatus(parentRoot);
91
+ const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot);
97
92
  if (!parentBlock) {
98
93
  throw new RegenError({
99
94
  code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
@@ -9,7 +9,7 @@ import {
9
9
  computeEpochAtSlot,
10
10
  computeStartSlotAtEpoch,
11
11
  } from "@lodestar/state-transition";
12
- import {BeaconBlock, RootHex, SignedBeaconBlock, Slot, isGloasBeaconBlock} from "@lodestar/types";
12
+ import {BeaconBlock, RootHex, SignedBeaconBlock, Slot} from "@lodestar/types";
13
13
  import {Logger, fromHex, toRootHex} from "@lodestar/utils";
14
14
  import {IBeaconDb} from "../../db/index.js";
15
15
  import {Metrics} from "../../metrics/index.js";
@@ -57,12 +57,7 @@ export class StateRegenerator implements IStateRegeneratorInternal {
57
57
  regenCaller: RegenCaller
58
58
  ): Promise<IBeaconStateView> {
59
59
  const parentRoot = toRootHex(block.parentRoot);
60
- const parentBlock = isGloasBeaconBlock(block)
61
- ? this.modules.forkChoice.getBlockHexAndBlockHash(
62
- parentRoot,
63
- toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
64
- )
65
- : this.modules.forkChoice.getBlockHexDefaultStatus(parentRoot);
60
+ const parentBlock = this.modules.forkChoice.getBlockHexDefaultStatus(parentRoot);
66
61
  if (!parentBlock) {
67
62
  throw new RegenError({
68
63
  code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
@@ -1,7 +1,7 @@
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";
@@ -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;
@@ -39,6 +40,7 @@ export type SeenPayloadEnvelopeInputModules = {
39
40
  export class SeenPayloadEnvelopeInput {
40
41
  private readonly config: ChainForkConfig;
41
42
  private readonly clock: IClock;
43
+ private readonly forkChoice: IForkChoice;
42
44
  private readonly chainEvents: ChainEventEmitter;
43
45
  private readonly signal: AbortSignal;
44
46
  private readonly serializedCache: SerializedCache;
@@ -46,9 +48,19 @@ export class SeenPayloadEnvelopeInput {
46
48
  private readonly logger?: Logger;
47
49
  private payloadInputs = new Map<RootHex, PayloadEnvelopeInput>();
48
50
 
49
- constructor({config, clock, chainEvents, signal, serializedCache, metrics, logger}: SeenPayloadEnvelopeInputModules) {
51
+ constructor({
52
+ config,
53
+ clock,
54
+ forkChoice,
55
+ chainEvents,
56
+ signal,
57
+ serializedCache,
58
+ metrics,
59
+ logger,
60
+ }: SeenPayloadEnvelopeInputModules) {
50
61
  this.config = config;
51
62
  this.clock = clock;
63
+ this.forkChoice = forkChoice;
52
64
  this.chainEvents = chainEvents;
53
65
  this.signal = signal;
54
66
  this.serializedCache = serializedCache;
@@ -67,14 +79,27 @@ export class SeenPayloadEnvelopeInput {
67
79
  });
68
80
  }
69
81
 
70
- this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.onFinalized);
82
+ this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
71
83
  this.signal.addEventListener("abort", () => {
72
- this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.onFinalized);
84
+ this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.pruneFinalized);
73
85
  });
74
86
  }
75
87
 
76
- private onFinalized = (checkpoint: CheckpointWithHex): void => {
77
- this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch));
88
+ private pruneFinalized = (checkpoint: CheckpointWithHex): void => {
89
+ const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch);
90
+ let deletedCount = 0;
91
+ for (const [, input] of this.payloadInputs) {
92
+ if (input.slot < finalizedSlot) {
93
+ this.evictPayloadInput(input);
94
+ deletedCount++;
95
+ }
96
+ }
97
+
98
+ this.logger?.debug("SeenPayloadEnvelopeInput.pruneFinalized deleted entries", {
99
+ finalizedSlot,
100
+ finalizedRoot: checkpoint.rootHex,
101
+ deletedCount,
102
+ });
78
103
  };
79
104
 
80
105
  add(props: Omit<CreateFromBlockProps, "daOutOfRange">): PayloadEnvelopeInput {
@@ -110,15 +135,19 @@ export class SeenPayloadEnvelopeInput {
110
135
  return this.payloadInputs.size;
111
136
  }
112
137
 
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++;
138
+ pruneBelowParent(parentBlock: ProtoBlock): void {
139
+ for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
140
+ if (block.slot < parentBlock.slot) {
141
+ const input = this.payloadInputs.get(block.blockRoot);
142
+ if (input) {
143
+ this.evictPayloadInput(input);
144
+ this.logger?.verbose("SeenPayloadEnvelopeInput.pruneBelowParent deleted", {
145
+ slot: block.slot,
146
+ root: block.blockRoot,
147
+ });
148
+ }
119
149
  }
120
150
  }
121
- this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount});
122
151
  }
123
152
 
124
153
  private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void {
@@ -1,28 +1,31 @@
1
- import {Slot, ValidatorIndex} from "@lodestar/types";
1
+ import {RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
2
  import {MapDef} from "@lodestar/utils";
3
3
 
4
4
  /**
5
- * Tracks signed proposer preferences we've already seen per (proposal_slot, validator_index).
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 validatorIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
8
+ private readonly validatorByDependentRootBySlot = new MapDef<Slot, Map<RootHex, ValidatorIndex>>(
9
+ () => new Map<RootHex, ValidatorIndex>()
10
+ );
9
11
 
10
- isKnown(proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
11
- return this.validatorIndexesBySlot.get(proposalSlot)?.has(validatorIndex) === true;
12
+ isKnown(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
13
+ return this.validatorByDependentRootBySlot.get(proposalSlot)?.get(dependentRoot) === validatorIndex;
12
14
  }
13
15
 
14
- add(proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
15
- this.validatorIndexesBySlot.getOrDefault(proposalSlot).add(validatorIndex);
16
+ add(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
17
+ this.validatorByDependentRootBySlot.getOrDefault(proposalSlot).set(dependentRoot, validatorIndex);
16
18
  }
17
19
 
18
20
  /**
19
- * Entries are only load-bearing while `proposal_slot > state.slot`. Once the slot has passed the
20
- * `[IGNORE] proposal_slot > state.slot` gossip rule takes over, so drop them on each slot tick.
21
+ * Entries are only load-bearing while `proposal_slot > current_slot`. Once the slot has
22
+ * passed the `[IGNORE] proposal_slot > current_slot` gossip rule takes over, so drop them
23
+ * on each slot tick.
21
24
  */
22
25
  prune(currentSlot: Slot): void {
23
- for (const slot of this.validatorIndexesBySlot.keys()) {
26
+ for (const slot of this.validatorByDependentRootBySlot.keys()) {
24
27
  if (slot < currentSlot) {
25
- this.validatorIndexesBySlot.delete(slot);
28
+ this.validatorByDependentRootBySlot.delete(slot);
26
29
  }
27
30
  }
28
31
  }