@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
@@ -128,6 +128,7 @@ export async function importBlock(
128
128
  blockDelaySec,
129
129
  currentSlot,
130
130
  fork >= ForkSeq.gloas ? ExecutionStatus.PayloadSeparated : executionStatus,
131
+ // TODO GLOAS: this is not useful post-gloas, may need to remove it?
131
132
  dataAvailabilityStatus
132
133
  );
133
134
 
@@ -135,8 +136,9 @@ export async function importBlock(
135
136
  // Some block event handlers require state being in state cache so need to do this before emitting EventType.block
136
137
  this.regen.processState(blockRootHex, postState);
137
138
 
138
- // For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import
139
- if (fork >= ForkSeq.gloas) {
139
+ // For range sync, PayloadEnvelope is created before reaching this
140
+ // we also don't need to trigger getBlobs() in that case
141
+ if (fork >= ForkSeq.gloas && !opts.fromRangeSync) {
140
142
  const payloadInput = this.seenPayloadEnvelopeInputCache.add({
141
143
  blockRootHex,
142
144
  block: block as SignedBeaconBlock<ForkPostGloas>,
@@ -1,14 +1,17 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
3
- import {SLOTS_PER_EPOCH} from "@lodestar/params";
4
- import {getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas} from "@lodestar/state-transition";
5
- import {fromHex, toRootHex} from "@lodestar/utils";
3
+ import {isStatePostGloas} from "@lodestar/state-transition";
4
+ import {fromHex} from "@lodestar/utils";
6
5
  import {ExecutionPayloadStatus} from "../../execution/index.js";
7
6
  import {isQueueErrorAborted} from "../../util/queue/index.js";
8
7
  import {BeaconChain} from "../chain.js";
9
8
  import {RegenCaller} from "../regen/interface.js";
10
9
  import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
11
10
  import {ImportPayloadOpts} from "./types.js";
11
+ import {
12
+ verifyExecutionPayloadEnvelope,
13
+ verifyExecutionPayloadEnvelopeSignature,
14
+ } from "./verifyExecutionPayloadEnvelope.js";
12
15
  import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
13
16
 
14
17
  const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64;
@@ -17,7 +20,7 @@ export enum PayloadErrorCode {
17
20
  EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID",
18
21
  EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR",
19
22
  BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE",
20
- STATE_TRANSITION_ERROR = "PAYLOAD_ERROR_STATE_TRANSITION_ERROR",
23
+ ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
21
24
  INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
22
25
  }
23
26
 
@@ -37,7 +40,7 @@ export type PayloadErrorType =
37
40
  blockRootHex: string;
38
41
  }
39
42
  | {
40
- code: PayloadErrorCode.STATE_TRANSITION_ERROR;
43
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR;
41
44
  message: string;
42
45
  }
43
46
  | {
@@ -69,38 +72,41 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
69
72
  /**
70
73
  * Import an execution payload envelope after all data is available.
71
74
  *
72
- * This function:
73
- * 1. Emits `execution_payload_available` if payload is for current slot
74
- * 2. Gets the ProtoBlock from fork choice
75
- * 3. Applies write-queue backpressure (waitForSpace) early, before verification
76
- * 4. Regenerates the block state
77
- * 5. Runs EL verification (notifyNewPayload) in parallel with signature verification and processExecutionPayloadEnvelope
78
- * 6. Persists verified payload envelope to hot DB
79
- * 7. Updates fork choice
80
- * 8. Caches the post-execution payload state
81
- * 9. Records metrics for column sources
82
- * 10. Emits `execution_payload` for recent enough payloads after successful import
75
+ * The envelope is only verified here, no state mutation. State effects from the payload
76
+ * are applied on the next block via processParentExecutionPayload.
83
77
  *
78
+ * The DA wait must have run upstream (range sync awaits DA in `verifyBlocksInEpoch` for the
79
+ * whole segment; gossip / API path uses the `processExecutionPayload` wrapper below).
80
+ *
81
+ * Steps:
82
+ * 1. Emit `execution_payload_available` event for payload attestation
83
+ * 2. Get the ProtoBlock from fork choice
84
+ * 3. Regenerate state for envelope verification
85
+ * 4. Verify envelope (fields against state, signature, and EL in parallel where possible)
86
+ * 5. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure)
87
+ * 6. Update fork choice (transitions the block's PENDING variant to FULL)
88
+ * 7. Record metrics for payload envelope and column sources
89
+ * 8. Emit `execution_payload` event
84
90
  */
85
91
  export async function importExecutionPayload(
86
92
  this: BeaconChain,
87
93
  payloadInput: PayloadEnvelopeInput,
88
- signal: AbortSignal,
89
94
  opts: ImportPayloadOpts = {}
90
95
  ): Promise<void> {
91
96
  const signedEnvelope = payloadInput.getPayloadEnvelope();
92
97
  const envelope = signedEnvelope.message;
98
+ const slot = envelope.payload.slotNumber;
93
99
  const blockRootHex = payloadInput.blockRootHex;
94
100
  const blockHashHex = payloadInput.getBlockHashHex();
95
- const fork = this.config.getForkName(envelope.payload.slotNumber);
101
+ const fork = this.config.getForkName(slot);
96
102
 
97
- // 1. Emit `execution_payload_available` event at the start of import. At this point the payload input
98
- // is already complete, so the payload and required data are available for payload attestation.
99
- // This event is only about availability, not validity of the execution payload, hence we can emit
100
- // it before getting a response from the execution client on whether the payload is valid or not.
101
- if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
103
+ // 1. Emit `execution_payload_available` event at the start of import. At this point the
104
+ // payload input is already complete, so the payload and required data are available for
105
+ // payload attestation. This event only signals availability (not validity), so we can emit
106
+ // it before getting a response from the EL on whether the payload is valid or not.
107
+ if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
102
108
  this.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
103
- slot: envelope.payload.slotNumber,
109
+ slot,
104
110
  blockRoot: blockRootHex,
105
111
  });
106
112
  }
@@ -114,16 +120,7 @@ export async function importExecutionPayload(
114
120
  });
115
121
  }
116
122
 
117
- // 3. Wait for data columns to be available before claiming a write-queue slot.
118
- // The helper is shared with future gloas sync services; take the single-item batch form here.
119
- await verifyPayloadsDataAvailability([payloadInput], signal);
120
-
121
- // 4. Apply backpressure from the write queue, before doing verification work.
122
- // The actual DB write is deferred until after verification succeeds.
123
- await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
124
-
125
- // 5. Get pre-state for processExecutionPayloadEnvelope
126
- // We need the block state (post-block, pre-payload) to process the envelope
123
+ // 3. Regenerate state for envelope verification
127
124
  const blockState = await this.regen.getBlockSlotState(
128
125
  protoBlock,
129
126
  protoBlock.slot,
@@ -132,13 +129,30 @@ export async function importExecutionPayload(
132
129
  );
133
130
  if (!isStatePostGloas(blockState)) {
134
131
  throw new PayloadError({
135
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
136
- message: `Expected gloas+ block state for payload import, got fork=${blockState.forkName}`,
132
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
133
+ message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`,
137
134
  });
138
135
  }
139
136
 
140
- // 6. Run verification steps in parallel
141
- const [execResult, signatureValid, postPayloadResult] = await Promise.all([
137
+ // 4. Verify envelope fields against state first to fail fast before the EL + BLS work.
138
+ // When validSignature is true, gossip/API has already verified both the signature and the
139
+ // executionRequestsRoot, so we skip those checks here.
140
+ try {
141
+ verifyExecutionPayloadEnvelope(this.config, blockState, envelope, {
142
+ verifyExecutionRequestsRoot: !opts.validSignature,
143
+ });
144
+ } catch (e) {
145
+ throw new PayloadError(
146
+ {
147
+ code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
148
+ message: (e as Error).message,
149
+ },
150
+ `Envelope verification error: ${(e as Error).message}`
151
+ );
152
+ }
153
+
154
+ // 4a. Run EL and signature verification in parallel
155
+ const [execResult, signatureValid] = await Promise.all([
142
156
  this.executionEngine.notifyNewPayload(
143
157
  fork,
144
158
  envelope.payload,
@@ -149,45 +163,22 @@ export async function importExecutionPayload(
149
163
 
150
164
  opts.validSignature === true
151
165
  ? Promise.resolve(true)
152
- : (async () => {
153
- const signatureSet = getExecutionPayloadEnvelopeSignatureSet(
154
- this.config,
155
- this.pubkeyCache,
156
- blockState,
157
- signedEnvelope,
158
- payloadInput.proposerIndex
159
- );
160
- return this.bls.verifySignatureSets([signatureSet]);
161
- })(),
162
-
163
- // Signature verified separately above.
164
- // State root check is done separately below with better error typing (matching block pipeline pattern).
165
- (async () => {
166
- try {
167
- return {
168
- postPayloadState: blockState.processExecutionPayloadEnvelope(signedEnvelope, {
169
- verifySignature: false,
170
- verifyStateRoot: false,
171
- }),
172
- };
173
- } catch (e) {
174
- throw new PayloadError(
175
- {
176
- code: PayloadErrorCode.STATE_TRANSITION_ERROR,
177
- message: (e as Error).message,
178
- },
179
- `State transition error: ${(e as Error).message}`
180
- );
181
- }
182
- })(),
166
+ : verifyExecutionPayloadEnvelopeSignature(
167
+ this.config,
168
+ blockState,
169
+ this.pubkeyCache,
170
+ signedEnvelope,
171
+ payloadInput.proposerIndex,
172
+ this.bls
173
+ ),
183
174
  ]);
184
175
 
185
- // 5a. Check signature verification result
176
+ // 4b. Check signature verification result
186
177
  if (!signatureValid) {
187
178
  throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE});
188
179
  }
189
180
 
190
- // 5b. Handle EL response
181
+ // 4c. Handle EL response
191
182
  switch (execResult.status) {
192
183
  case ExecutionPayloadStatus.VALID:
193
184
  break;
@@ -213,47 +204,33 @@ export async function importExecutionPayload(
213
204
  });
214
205
  }
215
206
 
216
- // 5c. Compute post-payload state root
217
- const postPayloadState = postPayloadResult.postPayloadState;
218
- const postPayloadStateRoot = postPayloadState.hashTreeRoot();
219
-
220
- // 6. Persist payload envelope to hot DB (performed asynchronously to avoid blocking)
207
+ // 5. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure
208
+ // on the import pipeline during sync, then perform the write asynchronously to avoid blocking.
209
+ await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
221
210
  this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
222
211
  if (!isQueueErrorAborted(e)) {
223
212
  this.logger.error(
224
213
  "Error pushing payload envelope to unfinalized write queue",
225
- {slot: envelope.payload.slotNumber, blockRoot: blockRootHex},
214
+ {slot, blockRoot: blockRootHex},
226
215
  e as Error
227
216
  );
228
217
  }
229
218
  });
230
219
 
231
- // 7. Update fork choice
232
- this.forkChoice.onExecutionPayload(
233
- blockRootHex,
234
- blockHashHex,
235
- envelope.payload.blockNumber,
236
- toRootHex(postPayloadStateRoot),
237
- toForkChoiceExecutionStatus(execResult.status)
238
- );
239
-
240
- // 8. Cache payload state
241
- this.regen.processState(blockRootHex, postPayloadState);
242
- if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) {
243
- const {checkpoint} = postPayloadState.computeAnchorCheckpoint();
244
- this.regen.addCheckpointState(checkpoint, postPayloadState);
245
- }
220
+ // 6. Update fork choice, transitions the block's PENDING variant to FULL
221
+ const execStatus = toForkChoiceExecutionStatus(execResult.status);
222
+ this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus);
246
223
 
247
- // 9. Record metrics for payload envelope and column sources
224
+ // 7. Record metrics for payload envelope and column sources
248
225
  this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
249
226
  for (const {source} of payloadInput.getSampledColumnsWithSource()) {
250
227
  this.metrics?.importPayload.columnsBySource.inc({source});
251
228
  }
252
229
 
253
- // 10. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
254
- if (this.clock.currentSlot - envelope.payload.slotNumber < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
230
+ // 8. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
231
+ if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
255
232
  this.emitter.emit(routes.events.EventType.executionPayload, {
256
- slot: envelope.payload.slotNumber,
233
+ slot,
257
234
  builderIndex: envelope.builderIndex,
258
235
  blockHash: blockHashHex,
259
236
  blockRoot: blockRootHex,
@@ -263,9 +240,27 @@ export async function importExecutionPayload(
263
240
  }
264
241
 
265
242
  this.logger.verbose("Execution payload imported", {
266
- slot: envelope.payload.slotNumber,
243
+ slot,
267
244
  builderIndex: envelope.builderIndex,
268
245
  blockRoot: blockRootHex,
269
246
  blockHash: blockHashHex,
270
247
  });
271
248
  }
249
+
250
+ /**
251
+ * Process an execution payload envelope end-to-end: wait for DA, then import.
252
+ *
253
+ * Used by the PayloadEnvelopeProcessor queue (gossip / API / unknown-payload sync) — i.e.
254
+ * callers that have NOT already awaited DA themselves. Range sync's inline dispatch in
255
+ * processBlocks skips this wrapper and calls `importExecutionPayload` directly, since
256
+ * `verifyBlocksInEpoch` already awaited DA for the segment.
257
+ */
258
+ export async function processExecutionPayload(
259
+ this: BeaconChain,
260
+ payloadInput: PayloadEnvelopeInput,
261
+ signal: AbortSignal,
262
+ opts: ImportPayloadOpts = {}
263
+ ): Promise<void> {
264
+ await verifyPayloadsDataAvailability([payloadInput], signal);
265
+ await importExecutionPayload.call(this, payloadInput, opts);
266
+ }
@@ -1,4 +1,4 @@
1
- import {SignedBeaconBlock} from "@lodestar/types";
1
+ import {SignedBeaconBlock, Slot} from "@lodestar/types";
2
2
  import {isErrorAborted, toRootHex} from "@lodestar/utils";
3
3
  import {Metrics} from "../../metrics/metrics.js";
4
4
  import {nextEventLoop} from "../../util/eventLoop.js";
@@ -8,6 +8,8 @@ import {BlockError, BlockErrorCode, isBlockErrorAborted} from "../errors/index.j
8
8
  import {BlockProcessOpts} from "../options.js";
9
9
  import {IBlockInput} from "./blockInput/types.js";
10
10
  import {importBlock} from "./importBlock.js";
11
+ import {importExecutionPayload} from "./importExecutionPayload.js";
12
+ import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
11
13
  import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
12
14
  import {assertLinearChainSegment} from "./utils/chainSegment.js";
13
15
  import {verifyBlocksInEpoch} from "./verifyBlock.js";
@@ -21,20 +23,24 @@ const QUEUE_MAX_LENGTH = 256;
21
23
  * BlockProcessor processes block jobs in a queued fashion, one after the other.
22
24
  */
23
25
  export class BlockProcessor {
24
- readonly jobQueue: JobItemQueue<[IBlockInput[], ImportBlockOpts], void>;
26
+ readonly jobQueue: JobItemQueue<[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts], void>;
25
27
 
26
28
  constructor(chain: BeaconChain, metrics: Metrics | null, opts: BlockProcessOpts, signal: AbortSignal) {
27
- this.jobQueue = new JobItemQueue<[IBlockInput[], ImportBlockOpts], void>(
28
- (job, importOpts) => {
29
- return processBlocks.call(chain, job, {...opts, ...importOpts});
29
+ this.jobQueue = new JobItemQueue<[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts], void>(
30
+ (job, payloadEnvelopes, importOpts) => {
31
+ return processBlocks.call(chain, job, payloadEnvelopes, {...opts, ...importOpts});
30
32
  },
31
33
  {maxLength: QUEUE_MAX_LENGTH, noYieldIfOneItem: true, signal},
32
34
  metrics?.blockProcessorQueue ?? undefined
33
35
  );
34
36
  }
35
37
 
36
- async processBlocksJob(job: IBlockInput[], opts: ImportBlockOpts = {}): Promise<void> {
37
- await this.jobQueue.push(job, opts);
38
+ async processBlocksJob(
39
+ job: IBlockInput[],
40
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
41
+ opts: ImportBlockOpts = {}
42
+ ): Promise<void> {
43
+ await this.jobQueue.push(job, payloadEnvelopes, opts);
38
44
  }
39
45
  }
40
46
 
@@ -51,16 +57,13 @@ export class BlockProcessor {
51
57
  export async function processBlocks(
52
58
  this: BeaconChain,
53
59
  blocks: IBlockInput[],
60
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
54
61
  opts: BlockProcessOpts & ImportBlockOpts
55
62
  ): Promise<void> {
56
63
  if (blocks.length === 0) {
57
64
  return; // TODO: or throw?
58
65
  }
59
66
 
60
- if (blocks.length > 1) {
61
- assertLinearChainSegment(this.config, blocks);
62
- }
63
-
64
67
  try {
65
68
  const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, opts);
66
69
 
@@ -70,10 +73,25 @@ export async function processBlocks(
70
73
  return;
71
74
  }
72
75
 
76
+ const {warnings: orphanedPayloads} = assertLinearChainSegment(
77
+ this.config,
78
+ relevantBlocks,
79
+ payloadEnvelopes,
80
+ parentBlock
81
+ );
82
+ if (orphanedPayloads != null) {
83
+ for (const orphaned of orphanedPayloads) {
84
+ this.logger.debug("Orphaned payload envelope in chain segment", {
85
+ slot: orphaned.slot,
86
+ blockRoot: orphaned.payloadEnvelopeInput.blockRootHex,
87
+ });
88
+ }
89
+ }
90
+
73
91
  // Fully verify a block to be imported immediately after. Does not produce any side-effects besides adding intermediate
74
92
  // states in the state cache through regen.
75
93
  const {postStates, dataAvailabilityStatuses, proposerBalanceDeltas, segmentExecStatus, indexedAttestationsByBlock} =
76
- await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, opts);
94
+ await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopes, opts);
77
95
 
78
96
  // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid
79
97
  // and we need to further propagate
@@ -89,7 +107,6 @@ export async function processBlocks(
89
107
  (block, i): FullyVerifiedBlock => ({
90
108
  blockInput: block,
91
109
  postState: postStates[i],
92
- postPayloadState: null,
93
110
  parentBlockSlot: parentSlots[i],
94
111
  executionStatus: executionStatuses[i],
95
112
  // start supporting optimistic syncing/processing
@@ -104,6 +121,20 @@ export async function processBlocks(
104
121
  for (const fullyVerifiedBlock of fullyVerifiedBlocks) {
105
122
  // TODO: Consider batching importBlock too if it takes significant time
106
123
  await importBlock.call(this, fullyVerifiedBlock, opts);
124
+
125
+ const slot = fullyVerifiedBlock.blockInput.getBlock().message.slot;
126
+ const payloadInput = payloadEnvelopes?.get(slot);
127
+ if (payloadInput?.hasPayloadEnvelope()) {
128
+ if (!payloadInput.isComplete()) {
129
+ // we validated DA before reaching this
130
+ throw new Error(`Payload envelope for slot ${slot} not complete after DA verification`);
131
+ }
132
+ // we already awaited DA in verifyBlocksInEpoch for this segment
133
+ // TODO GLOAS: may need FullyVerifiedPayload here with DatAvailabilityStatus added from here
134
+ // the current flow use that data from the forkchoice pending node which is not correct
135
+ await importExecutionPayload.call(this, payloadInput, {validSignature: false});
136
+ }
137
+
107
138
  await nextEventLoop();
108
139
  }
109
140
  } catch (e) {
@@ -2,7 +2,7 @@ import {Metrics} from "../../metrics/metrics.js";
2
2
  import {JobItemQueue} from "../../util/queue/index.js";
3
3
  import type {BeaconChain} from "../chain.js";
4
4
  import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
5
- import {importExecutionPayload} from "./importExecutionPayload.js";
5
+ import {processExecutionPayload} from "./importExecutionPayload.js";
6
6
  import {ImportPayloadOpts} from "./types.js";
7
7
 
8
8
  // TODO GLOAS: Set to be equal to DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES for now
@@ -30,7 +30,7 @@ export class PayloadEnvelopeProcessor {
30
30
  this.jobQueue = new JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>(
31
31
  (payloadInput, opts) => {
32
32
  this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.importing);
33
- return importExecutionPayload.call(chain, payloadInput, signal, opts);
33
+ return processExecutionPayload.call(chain, payloadInput, signal, opts);
34
34
  },
35
35
  {maxLength: QUEUE_MAX_LENGTH, noYieldIfOneItem: true, signal},
36
36
  metrics?.payloadEnvelopeProcessorQueue ?? undefined
@@ -1,5 +1,5 @@
1
1
  import type {ChainForkConfig} from "@lodestar/config";
2
- import {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
2
+ import type {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
3
3
  import {ForkSeq} from "@lodestar/params";
4
4
  import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
5
5
  import type {IndexedAttestation, Slot, fulu} from "@lodestar/types";
@@ -43,8 +43,9 @@ export enum BlobSidecarValidation {
43
43
 
44
44
  export type ImportPayloadOpts = {
45
45
  /**
46
- * Set to true if envelope signature was already verified (e.g., during gossip/API validation).
47
- * When false/undefined, signature will be verified during import.
46
+ * Set to true when the envelope was already validated upstream (e.g., gossip/API validation):
47
+ * signature is trusted and execution_requests_root was already verified against the bid.
48
+ * When false/undefined, both are verified during import.
48
49
  */
49
50
  validSignature?: boolean;
50
51
  };
@@ -88,7 +89,14 @@ export type ImportBlockOpts = {
88
89
  seenTimestampSec?: number;
89
90
  };
90
91
 
91
- type FullyVerifiedBlockBase = {
92
+ /**
93
+ * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import.
94
+ *
95
+ * `executionStatus` reflects the outcome of execution payload verification at block-import time:
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)
98
+ */
99
+ export type FullyVerifiedBlock = {
92
100
  blockInput: IBlockInput;
93
101
  postState: IBeaconStateView;
94
102
  parentBlockSlot: Slot;
@@ -98,25 +106,6 @@ type FullyVerifiedBlockBase = {
98
106
  indexedAttestations: IndexedAttestation[];
99
107
  /** Seen timestamp seconds */
100
108
  seenTimestampSec: number;
109
+ /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */
110
+ executionStatus: BlockExecutionStatus | PayloadExecutionStatus;
101
111
  };
102
-
103
- /**
104
- * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import.
105
- *
106
- * Discriminated union on `postPayloadState`:
107
- * - `null` → block has no pre-verified envelope; `executionStatus` is any `BlockExecutionStatus`
108
- * - non-null → envelope was pre-verified during state transition; `executionStatus` is narrowed to
109
- * `Valid | Syncing` (matching what `forkChoice.onExecutionPayload` expects)
110
- */
111
- export type FullyVerifiedBlock = FullyVerifiedBlockBase &
112
- (
113
- | {
114
- postPayloadState: null;
115
- /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */
116
- executionStatus: BlockExecutionStatus;
117
- }
118
- | {
119
- postPayloadState: IBeaconStateView;
120
- executionStatus: PayloadExecutionStatus;
121
- }
122
- );
@@ -1,29 +1,118 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {ssz} from "@lodestar/types";
2
+ import {ProtoBlock} from "@lodestar/fork-choice";
3
+ import {Slot, isGloasBeaconBlock, ssz} from "@lodestar/types";
4
+ import {toRootHex} from "@lodestar/utils";
3
5
  import {BlockError, BlockErrorCode} from "../../errors/index.js";
4
6
  import {IBlockInput} from "../blockInput/types.js";
7
+ import {PayloadEnvelopeInput} from "../payloadEnvelopeInput/payloadEnvelopeInput.js";
8
+
9
+ export type OrphanedPayloadEnvelope = {
10
+ slot: Slot;
11
+ payloadEnvelopeInput: PayloadEnvelopeInput;
12
+ };
13
+
14
+ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
5
15
 
6
16
  /**
7
- * Assert this chain segment of blocks is linear with slot numbers and hashes
17
+ * Assert this chain segment of blocks is linear with slot numbers and hashes,
18
+ * and that the provided envelopes are consistent with their respective blocks.
19
+ *
20
+ * Must be called after verifyBlocksSanityChecks so that parentBlock (from forkchoice)
21
+ * is available to seed the execution hash chain.
22
+ *
23
+ * For each block:
24
+ * - Verifies parent root + slot linearity
25
+ * - For gloas: verifies bid.parentBlockHash matches the tracked execution hash; if not, the
26
+ * previous FULL envelope is treated as orphaned (segment continues as if previous slot was EMPTY)
27
+ * - If an envelope exists for this slot: verifies it references this block's root
28
+ * - Advances the tracked execution hash (FULL if envelope present, EMPTY if not)
8
29
  */
30
+ export function assertLinearChainSegment(
31
+ config: ChainForkConfig,
32
+ blocks: IBlockInput[],
33
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
34
+ parentBlock: ProtoBlock
35
+ ): ChainSegmentResult {
36
+ const warnings: OrphanedPayloadEnvelope[] = [];
9
37
 
10
- export function assertLinearChainSegment(config: ChainForkConfig, blocks: IBlockInput[]): void {
11
- for (let i = 0; i < blocks.length - 1; i++) {
38
+ // Track the expected execution payload block hash through the segment.
39
+ // Starts from the known forkchoice parent's execution hash.
40
+ // - FULL variant (envelope present for slot): advances to envelope.payload.blockHash
41
+ // - 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;
44
+ // Track the execution hash before the last FULL advancement so we can recover
45
+ // if the next block reveals that envelope was orphaned.
46
+ let prevExecHash: string | null = currentExecHash;
47
+ // The slot whose envelope last advanced currentExecHash (for warning context).
48
+ let lastFullSlot: Slot | null = null;
49
+
50
+ for (let i = 0; i < blocks.length; i++) {
12
51
  const block = blocks[i].getBlock();
13
- const child = blocks[i + 1].getBlock();
14
- // If this block has a child in this chain segment, ensure that its parent root matches
15
- // the root of this block.
16
- if (
17
- !ssz.Root.equals(
18
- config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message),
19
- child.message.parentRoot
20
- )
21
- ) {
22
- throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS});
52
+ const slot = block.message.slot;
53
+
54
+ if (i > 0) {
55
+ const prevBlock = blocks[i - 1].getBlock();
56
+ // Ensure parent root matches the previous block's root
57
+ if (
58
+ !ssz.Root.equals(
59
+ config.getForkTypes(prevBlock.message.slot).BeaconBlock.hashTreeRoot(prevBlock.message),
60
+ block.message.parentRoot
61
+ )
62
+ ) {
63
+ throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS});
64
+ }
65
+ // Ensure slots are strictly increasing
66
+ if (slot <= prevBlock.message.slot) {
67
+ throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_SLOTS});
68
+ }
23
69
  }
24
- // Ensure that the slots are strictly increasing throughout the chain segment.
25
- if (child.message.slot <= block.message.slot) {
26
- throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_SLOTS});
70
+
71
+ if (isGloasBeaconBlock(block.message) && currentExecHash !== null) {
72
+ // Verify the bid's parentBlockHash matches the tracked execution hash.
73
+ // This ensures the block was built on the correct FULL or EMPTY variant of its parent.
74
+ const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
75
+ if (bidParentHash !== currentExecHash) {
76
+ // Maybe the previous slot's FULL envelope was orphaned — try falling back.
77
+ // If even prevExecHash doesn't match, the segment is non-linear.
78
+ if (bidParentHash !== prevExecHash) {
79
+ throw new BlockError(block, {
80
+ code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
81
+ parentRoot: toRootHex(block.message.parentRoot),
82
+ parentBlockHash: bidParentHash,
83
+ });
84
+ }
85
+ if (lastFullSlot !== null && payloadEnvelopes !== null) {
86
+ const orphanedInput = payloadEnvelopes.get(lastFullSlot);
87
+ if (orphanedInput != null) {
88
+ warnings.push({slot: lastFullSlot, payloadEnvelopeInput: orphanedInput});
89
+ }
90
+ }
91
+ currentExecHash = prevExecHash;
92
+ }
93
+
94
+ const payloadInput = payloadEnvelopes?.get(slot) ?? null;
95
+ const payloadEnvelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : null;
96
+ if (payloadEnvelope !== null) {
97
+ // Verify the envelope references this block's root
98
+ const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message));
99
+ const envelopeBlockRoot = toRootHex(payloadEnvelope.message.beaconBlockRoot);
100
+ if (blockRoot !== envelopeBlockRoot) {
101
+ throw new BlockError(block, {
102
+ code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
103
+ envelopeBlockRoot,
104
+ blockRoot,
105
+ });
106
+ }
107
+
108
+ // FULL variant: save state before advancing, then advance
109
+ prevExecHash = currentExecHash;
110
+ lastFullSlot = slot;
111
+ currentExecHash = toRootHex(payloadEnvelope.message.payload.blockHash);
112
+ }
113
+ // EMPTY variant: currentExecHash unchanged
27
114
  }
28
115
  }
116
+
117
+ return {warnings: warnings.length > 0 ? warnings : null};
29
118
  }