@lodestar/beacon-node 1.43.0-dev.3d6ae250a4 → 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 (265) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +16 -5
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +45 -2
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/debug/index.d.ts.map +1 -1
  8. package/lib/api/impl/debug/index.js +0 -1
  9. package/lib/api/impl/debug/index.js.map +1 -1
  10. package/lib/api/impl/lodestar/index.js +1 -1
  11. package/lib/api/impl/lodestar/index.js.map +1 -1
  12. package/lib/api/impl/validator/index.d.ts.map +1 -1
  13. package/lib/api/impl/validator/index.js +68 -2
  14. package/lib/api/impl/validator/index.js.map +1 -1
  15. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  16. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  17. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  18. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  19. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  20. package/lib/chain/blocks/importBlock.js +16 -28
  21. package/lib/chain/blocks/importBlock.js.map +1 -1
  22. package/lib/chain/blocks/importExecutionPayload.d.ts +19 -6
  23. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  24. package/lib/chain/blocks/importExecutionPayload.js +45 -23
  25. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  26. package/lib/chain/blocks/index.d.ts +5 -3
  27. package/lib/chain/blocks/index.d.ts.map +1 -1
  28. package/lib/chain/blocks/index.js +59 -25
  29. package/lib/chain/blocks/index.js.map +1 -1
  30. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +4 -0
  31. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  32. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +9 -2
  33. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  34. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +1 -0
  35. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  36. package/lib/chain/blocks/payloadEnvelopeProcessor.js +2 -2
  37. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  38. package/lib/chain/blocks/types.d.ts +4 -3
  39. package/lib/chain/blocks/types.d.ts.map +1 -1
  40. package/lib/chain/blocks/utils/chainSegment.d.ts +23 -2
  41. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  42. package/lib/chain/blocks/utils/chainSegment.js +89 -12
  43. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  44. package/lib/chain/blocks/verifyBlock.d.ts +5 -3
  45. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  46. package/lib/chain/blocks/verifyBlock.js +50 -7
  47. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  48. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  49. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  50. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  51. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  52. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  53. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  54. package/lib/chain/blocks/verifyBlocksSanityChecks.js +25 -5
  55. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  56. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  57. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  58. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +7 -4
  59. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  60. package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
  61. package/lib/chain/blocks/verifyPayloadsDataAvailability.js +8 -3
  62. package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
  63. package/lib/chain/chain.d.ts +3 -2
  64. package/lib/chain/chain.d.ts.map +1 -1
  65. package/lib/chain/chain.js +20 -11
  66. package/lib/chain/chain.js.map +1 -1
  67. package/lib/chain/emitter.d.ts +0 -11
  68. package/lib/chain/emitter.d.ts.map +1 -1
  69. package/lib/chain/emitter.js +0 -4
  70. package/lib/chain/emitter.js.map +1 -1
  71. package/lib/chain/errors/blockError.d.ts +8 -1
  72. package/lib/chain/errors/blockError.d.ts.map +1 -1
  73. package/lib/chain/errors/blockError.js +2 -0
  74. package/lib/chain/errors/blockError.js.map +1 -1
  75. package/lib/chain/errors/index.d.ts +1 -0
  76. package/lib/chain/errors/index.d.ts.map +1 -1
  77. package/lib/chain/errors/index.js +1 -0
  78. package/lib/chain/errors/index.js.map +1 -1
  79. package/lib/chain/errors/proposerPreferences.d.ts +40 -0
  80. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -0
  81. package/lib/chain/errors/proposerPreferences.js +14 -0
  82. package/lib/chain/errors/proposerPreferences.js.map +1 -0
  83. package/lib/chain/interface.d.ts +3 -2
  84. package/lib/chain/interface.d.ts.map +1 -1
  85. package/lib/chain/interface.js.map +1 -1
  86. package/lib/chain/opPools/payloadAttestationPool.d.ts +3 -2
  87. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  88. package/lib/chain/opPools/payloadAttestationPool.js +26 -4
  89. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  90. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  91. package/lib/chain/prepareNextSlot.js +16 -18
  92. package/lib/chain/prepareNextSlot.js.map +1 -1
  93. package/lib/chain/produceBlock/produceBlockBody.d.ts +12 -3
  94. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  95. package/lib/chain/produceBlock/produceBlockBody.js +35 -17
  96. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  97. package/lib/chain/regen/interface.d.ts +1 -0
  98. package/lib/chain/regen/interface.d.ts.map +1 -1
  99. package/lib/chain/regen/interface.js +1 -0
  100. package/lib/chain/regen/interface.js.map +1 -1
  101. package/lib/chain/regen/queued.d.ts.map +1 -1
  102. package/lib/chain/regen/queued.js +1 -4
  103. package/lib/chain/regen/queued.js.map +1 -1
  104. package/lib/chain/regen/regen.d.ts.map +1 -1
  105. package/lib/chain/regen/regen.js +1 -4
  106. package/lib/chain/regen/regen.js.map +1 -1
  107. package/lib/chain/seenCache/index.d.ts +1 -0
  108. package/lib/chain/seenCache/index.d.ts.map +1 -1
  109. package/lib/chain/seenCache/index.js +1 -0
  110. package/lib/chain/seenCache/index.js.map +1 -1
  111. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -5
  112. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  113. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +49 -15
  114. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  115. package/lib/chain/seenCache/seenProposerPreferences.d.ts +16 -0
  116. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -0
  117. package/lib/chain/seenCache/seenProposerPreferences.js +26 -0
  118. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -0
  119. package/lib/chain/validation/block.d.ts.map +1 -1
  120. package/lib/chain/validation/block.js +1 -0
  121. package/lib/chain/validation/block.js.map +1 -1
  122. package/lib/chain/validation/executionPayloadBid.js +11 -8
  123. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  124. package/lib/chain/validation/proposerPreferences.d.ts +8 -0
  125. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -0
  126. package/lib/chain/validation/proposerPreferences.js +91 -0
  127. package/lib/chain/validation/proposerPreferences.js.map +1 -0
  128. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  129. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  130. package/lib/metrics/metrics/lodestar.js +4 -0
  131. package/lib/metrics/metrics/lodestar.js.map +1 -1
  132. package/lib/network/gossip/interface.d.ts +7 -1
  133. package/lib/network/gossip/interface.d.ts.map +1 -1
  134. package/lib/network/gossip/interface.js +1 -0
  135. package/lib/network/gossip/interface.js.map +1 -1
  136. package/lib/network/gossip/scoringParameters.d.ts.map +1 -1
  137. package/lib/network/gossip/scoringParameters.js +12 -1
  138. package/lib/network/gossip/scoringParameters.js.map +1 -1
  139. package/lib/network/gossip/topic.d.ts +29 -766
  140. package/lib/network/gossip/topic.d.ts.map +1 -1
  141. package/lib/network/gossip/topic.js +6 -0
  142. package/lib/network/gossip/topic.js.map +1 -1
  143. package/lib/network/interface.d.ts +1 -0
  144. package/lib/network/interface.d.ts.map +1 -1
  145. package/lib/network/network.d.ts +1 -0
  146. package/lib/network/network.d.ts.map +1 -1
  147. package/lib/network/network.js +5 -0
  148. package/lib/network/network.js.map +1 -1
  149. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  150. package/lib/network/processor/gossipHandlers.js +38 -16
  151. package/lib/network/processor/gossipHandlers.js.map +1 -1
  152. package/lib/network/processor/gossipQueues/index.d.ts.map +1 -1
  153. package/lib/network/processor/gossipQueues/index.js +5 -0
  154. package/lib/network/processor/gossipQueues/index.js.map +1 -1
  155. package/lib/network/processor/index.d.ts.map +1 -1
  156. package/lib/network/processor/index.js +6 -5
  157. package/lib/network/processor/index.js.map +1 -1
  158. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  159. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +14 -6
  160. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  161. package/lib/network/reqresp/handlers/blobSidecarsByRange.d.ts.map +1 -1
  162. package/lib/network/reqresp/handlers/blobSidecarsByRange.js +11 -5
  163. package/lib/network/reqresp/handlers/blobSidecarsByRange.js.map +1 -1
  164. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  165. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +17 -5
  166. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  167. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  168. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +7 -4
  169. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  170. package/lib/node/nodejs.js +2 -2
  171. package/lib/node/nodejs.js.map +1 -1
  172. package/lib/sync/range/batch.d.ts +23 -2
  173. package/lib/sync/range/batch.d.ts.map +1 -1
  174. package/lib/sync/range/batch.js +132 -44
  175. package/lib/sync/range/batch.js.map +1 -1
  176. package/lib/sync/range/chain.d.ts +6 -2
  177. package/lib/sync/range/chain.d.ts.map +1 -1
  178. package/lib/sync/range/chain.js +26 -7
  179. package/lib/sync/range/chain.js.map +1 -1
  180. package/lib/sync/range/range.d.ts.map +1 -1
  181. package/lib/sync/range/range.js +17 -6
  182. package/lib/sync/range/range.js.map +1 -1
  183. package/lib/sync/types.d.ts +34 -0
  184. package/lib/sync/types.d.ts.map +1 -1
  185. package/lib/sync/types.js +34 -0
  186. package/lib/sync/types.js.map +1 -1
  187. package/lib/sync/unknownBlock.d.ts +22 -1
  188. package/lib/sync/unknownBlock.d.ts.map +1 -1
  189. package/lib/sync/unknownBlock.js +602 -53
  190. package/lib/sync/unknownBlock.js.map +1 -1
  191. package/lib/sync/utils/downloadByRange.d.ts +46 -10
  192. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  193. package/lib/sync/utils/downloadByRange.js +162 -24
  194. package/lib/sync/utils/downloadByRange.js.map +1 -1
  195. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  196. package/lib/sync/utils/downloadByRoot.js +16 -2
  197. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  198. package/lib/sync/utils/pendingBlocksTree.d.ts +0 -1
  199. package/lib/sync/utils/pendingBlocksTree.d.ts.map +1 -1
  200. package/lib/sync/utils/pendingBlocksTree.js +0 -9
  201. package/lib/sync/utils/pendingBlocksTree.js.map +1 -1
  202. package/lib/util/sszBytes.d.ts.map +1 -1
  203. package/lib/util/sszBytes.js +8 -6
  204. package/lib/util/sszBytes.js.map +1 -1
  205. package/package.json +15 -15
  206. package/src/api/impl/beacon/blocks/index.ts +21 -5
  207. package/src/api/impl/beacon/pool/index.ts +83 -1
  208. package/src/api/impl/debug/index.ts +0 -1
  209. package/src/api/impl/lodestar/index.ts +1 -1
  210. package/src/api/impl/validator/index.ts +82 -1
  211. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  212. package/src/chain/blocks/importBlock.ts +16 -48
  213. package/src/chain/blocks/importExecutionPayload.ts +59 -25
  214. package/src/chain/blocks/index.ts +73 -22
  215. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +10 -2
  216. package/src/chain/blocks/payloadEnvelopeInput/types.ts +1 -0
  217. package/src/chain/blocks/payloadEnvelopeProcessor.ts +2 -2
  218. package/src/chain/blocks/types.ts +4 -3
  219. package/src/chain/blocks/utils/chainSegment.ts +114 -17
  220. package/src/chain/blocks/verifyBlock.ts +70 -9
  221. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  222. package/src/chain/blocks/verifyBlocksSanityChecks.ts +26 -7
  223. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +9 -4
  224. package/src/chain/blocks/verifyPayloadsDataAvailability.ts +7 -4
  225. package/src/chain/chain.ts +26 -10
  226. package/src/chain/emitter.ts +0 -11
  227. package/src/chain/errors/blockError.ts +4 -1
  228. package/src/chain/errors/index.ts +1 -0
  229. package/src/chain/errors/proposerPreferences.ts +47 -0
  230. package/src/chain/interface.ts +7 -1
  231. package/src/chain/opPools/payloadAttestationPool.ts +29 -8
  232. package/src/chain/prepareNextSlot.ts +21 -29
  233. package/src/chain/produceBlock/produceBlockBody.ts +46 -22
  234. package/src/chain/regen/interface.ts +1 -0
  235. package/src/chain/regen/queued.ts +2 -7
  236. package/src/chain/regen/regen.ts +2 -7
  237. package/src/chain/seenCache/index.ts +1 -0
  238. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +67 -18
  239. package/src/chain/seenCache/seenProposerPreferences.ts +32 -0
  240. package/src/chain/validation/block.ts +1 -0
  241. package/src/chain/validation/executionPayloadBid.ts +11 -8
  242. package/src/chain/validation/proposerPreferences.ts +110 -0
  243. package/src/metrics/metrics/lodestar.ts +4 -0
  244. package/src/network/gossip/interface.ts +6 -0
  245. package/src/network/gossip/scoringParameters.ts +14 -1
  246. package/src/network/gossip/topic.ts +6 -0
  247. package/src/network/interface.ts +1 -0
  248. package/src/network/network.ts +11 -0
  249. package/src/network/processor/gossipHandlers.ts +53 -17
  250. package/src/network/processor/gossipQueues/index.ts +5 -0
  251. package/src/network/processor/index.ts +6 -5
  252. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +14 -6
  253. package/src/network/reqresp/handlers/blobSidecarsByRange.ts +11 -5
  254. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +17 -5
  255. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +7 -4
  256. package/src/node/nodejs.ts +2 -2
  257. package/src/sync/range/batch.ts +188 -49
  258. package/src/sync/range/chain.ts +37 -9
  259. package/src/sync/range/range.ts +18 -6
  260. package/src/sync/types.ts +72 -0
  261. package/src/sync/unknownBlock.ts +760 -57
  262. package/src/sync/utils/downloadByRange.ts +272 -39
  263. package/src/sync/utils/downloadByRoot.ts +24 -2
  264. package/src/sync/utils/pendingBlocksTree.ts +0 -15
  265. package/src/util/sszBytes.ts +8 -6
@@ -1,7 +1,8 @@
1
1
  import {routes} from "@lodestar/api";
2
- import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice";
3
- import {isStatePostGloas} from "@lodestar/state-transition";
4
- import {fromHex} from "@lodestar/utils";
2
+ import {ExecutionStatus, PayloadExecutionStatus, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
3
+ import {DataAvailabilityStatus, isStatePostGloas} from "@lodestar/state-transition";
4
+ import {isErrorAborted} from "@lodestar/utils";
5
+ import {ZERO_HASH_HEX} from "../../constants/index.js";
5
6
  import {ExecutionPayloadStatus} from "../../execution/index.js";
6
7
  import {isQueueErrorAborted} from "../../util/queue/index.js";
7
8
  import {BeaconChain} from "../chain.js";
@@ -60,7 +61,6 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
60
61
  switch (status) {
61
62
  case ExecutionPayloadStatus.VALID:
62
63
  return ExecutionStatus.Valid;
63
- // TODO GLOAS: Handle optimistic import for payload
64
64
  case ExecutionPayloadStatus.SYNCING:
65
65
  case ExecutionPayloadStatus.ACCEPTED:
66
66
  return ExecutionStatus.Syncing;
@@ -75,21 +75,24 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe
75
75
  * The envelope is only verified here, no state mutation. State effects from the payload
76
76
  * are applied on the next block via processParentExecutionPayload.
77
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
+ *
78
81
  * Steps:
79
82
  * 1. Emit `execution_payload_available` event for payload attestation
80
83
  * 2. Get the ProtoBlock from fork choice
81
- * 3. Wait for data columns to be available
82
- * 4. Regenerate state for envelope verification
83
- * 5. Verify envelope (fields against state, signature, and EL in parallel where possible)
84
- * 6. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure)
85
- * 7. Update fork choice (transitions the block's PENDING variant to FULL)
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. Queue notifyForkchoiceUpdate to engine api
86
89
  * 8. Record metrics for payload envelope and column sources
87
90
  * 9. Emit `execution_payload` event
88
91
  */
89
92
  export async function importExecutionPayload(
90
93
  this: BeaconChain,
91
94
  payloadInput: PayloadEnvelopeInput,
92
- signal: AbortSignal,
95
+ dataAvailabilityStatus: DataAvailabilityStatus,
93
96
  opts: ImportPayloadOpts = {}
94
97
  ): Promise<void> {
95
98
  const signedEnvelope = payloadInput.getPayloadEnvelope();
@@ -119,11 +122,7 @@ export async function importExecutionPayload(
119
122
  });
120
123
  }
121
124
 
122
- // 3. Wait for data columns to be available.
123
- // The helper is shared with future gloas sync services; take the single-item batch form here.
124
- await verifyPayloadsDataAvailability([payloadInput], signal);
125
-
126
- // 4. Regenerate state for envelope verification
125
+ // 3. Regenerate state for envelope verification
127
126
  const blockState = await this.regen.getBlockSlotState(
128
127
  protoBlock,
129
128
  protoBlock.slot,
@@ -137,7 +136,7 @@ export async function importExecutionPayload(
137
136
  });
138
137
  }
139
138
 
140
- // 5. Verify envelope fields against state first to fail fast before the EL + BLS work.
139
+ // 4. Verify envelope fields against state first to fail fast before the EL + BLS work.
141
140
  // When validSignature is true, gossip/API has already verified both the signature and the
142
141
  // executionRequestsRoot, so we skip those checks here.
143
142
  try {
@@ -154,13 +153,13 @@ export async function importExecutionPayload(
154
153
  );
155
154
  }
156
155
 
157
- // 5a. Run EL and signature verification in parallel
156
+ // 4a. Run EL and signature verification in parallel
158
157
  const [execResult, signatureValid] = await Promise.all([
159
158
  this.executionEngine.notifyNewPayload(
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
 
@@ -176,12 +175,12 @@ export async function importExecutionPayload(
176
175
  ),
177
176
  ]);
178
177
 
179
- // 5b. Check signature verification result
178
+ // 4b. Check signature verification result
180
179
  if (!signatureValid) {
181
180
  throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE});
182
181
  }
183
182
 
184
- // 5c. Handle EL response
183
+ // 4c. Handle EL response
185
184
  switch (execResult.status) {
186
185
  case ExecutionPayloadStatus.VALID:
187
186
  break;
@@ -207,7 +206,7 @@ export async function importExecutionPayload(
207
206
  });
208
207
  }
209
208
 
210
- // 6. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure
209
+ // 5. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure
211
210
  // on the import pipeline during sync, then perform the write asynchronously to avoid blocking.
212
211
  await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
213
212
  this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
@@ -220,9 +219,27 @@ export async function importExecutionPayload(
220
219
  }
221
220
  });
222
221
 
223
- // 7. Update fork choice, transitions the block's PENDING variant to FULL
222
+ // 6. Update fork choice, transitions the block's PENDING variant to FULL
224
223
  const execStatus = toForkChoiceExecutionStatus(execResult.status);
225
- this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus);
224
+ this.forkChoice.onExecutionPayload(
225
+ blockRootHex,
226
+ blockHashHex,
227
+ envelope.payload.blockNumber,
228
+ execStatus,
229
+ dataAvailabilityStatus
230
+ );
231
+
232
+ // 7. Queue notifyForkchoiceUpdate to engine api
233
+ const head = this.forkChoice.getHead();
234
+ if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) {
235
+ const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
236
+ const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
237
+ this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => {
238
+ if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
239
+ this.logger.error("Error pushing notifyForkchoiceUpdate()", {blockHashHex, finalizedBlockHash}, e);
240
+ }
241
+ });
242
+ }
226
243
 
227
244
  // 8. Record metrics for payload envelope and column sources
228
245
  this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source});
@@ -237,8 +254,7 @@ export async function importExecutionPayload(
237
254
  builderIndex: envelope.builderIndex,
238
255
  blockHash: blockHashHex,
239
256
  blockRoot: blockRootHex,
240
- // TODO GLOAS: revisit once we support optimistic import
241
- executionOptimistic: false,
257
+ executionOptimistic: execStatus === ExecutionStatus.Syncing,
242
258
  });
243
259
  }
244
260
 
@@ -249,3 +265,21 @@ export async function importExecutionPayload(
249
265
  blockHash: blockHashHex,
250
266
  });
251
267
  }
268
+
269
+ /**
270
+ * Process an execution payload envelope end-to-end: wait for DA, then import.
271
+ *
272
+ * Used by the PayloadEnvelopeProcessor queue (gossip / API / unknown-payload sync) — i.e.
273
+ * callers that have NOT already awaited DA themselves. Range sync's inline dispatch in
274
+ * processBlocks skips this wrapper and calls `importExecutionPayload` directly, since
275
+ * `verifyBlocksInEpoch` already awaited DA for the segment.
276
+ */
277
+ export async function processExecutionPayload(
278
+ this: BeaconChain,
279
+ payloadInput: PayloadEnvelopeInput,
280
+ signal: AbortSignal,
281
+ opts: ImportPayloadOpts = {}
282
+ ): Promise<void> {
283
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability([payloadInput], signal);
284
+ await importExecutionPayload.call(this, payloadInput, dataAvailabilityStatuses[0], opts);
285
+ }
@@ -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,18 +57,15 @@ 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
- const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, opts);
68
+ const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, payloadEnvelopes, opts);
66
69
 
67
70
  // No relevant blocks, skip verifyBlocksInEpoch()
68
71
  if (relevantBlocks.length === 0 || parentBlock === null) {
@@ -70,10 +73,31 @@ 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
- const {postStates, dataAvailabilityStatuses, proposerBalanceDeltas, segmentExecStatus, indexedAttestationsByBlock} =
76
- await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, opts);
93
+ const {
94
+ postStates,
95
+ blockDAStatuses,
96
+ payloadDAStatuses,
97
+ proposerBalanceDeltas,
98
+ segmentExecStatus,
99
+ indexedAttestationsByBlock,
100
+ } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopes, opts);
77
101
 
78
102
  // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid
79
103
  // and we need to further propagate
@@ -85,24 +109,51 @@ export async function processBlocks(
85
109
  }
86
110
 
87
111
  const {executionStatuses} = segmentExecStatus;
88
- const fullyVerifiedBlocks = relevantBlocks.map(
89
- (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, {
90
116
  blockInput: block,
91
117
  postState: postStates[i],
92
118
  parentBlockSlot: parentSlots[i],
93
119
  executionStatus: executionStatuses[i],
94
120
  // start supporting optimistic syncing/processing
95
- dataAvailabilityStatus: dataAvailabilityStatuses[i],
121
+ dataAvailabilityStatus: blockDAStatuses[i],
96
122
  proposerBalanceDelta: proposerBalanceDeltas[i],
97
123
  indexedAttestations: indexedAttestationsByBlock[i],
98
124
  // TODO: Make this param mandatory and capture in gossip
99
125
  seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000),
100
- })
101
- );
126
+ });
127
+ }
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)));
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
+ }
142
+
143
+ const payloadInput = payloadEnvelopes?.get(slot);
144
+ if (payloadInput?.hasPayloadEnvelope()) {
145
+ if (!payloadInput.isComplete()) {
146
+ // we validated DA before reaching this
147
+ throw new Error(`Payload envelope for slot ${slot} not complete after DA verification`);
148
+ }
149
+ // we already awaited DA in verifyBlocksInEpoch for this segment
150
+ const payloadDA = payloadDAStatuses.get(slot);
151
+ if (payloadDA === undefined) {
152
+ throw new Error(`Missing payload DA status for slot ${slot}`);
153
+ }
154
+ await importExecutionPayload.call(this, payloadInput, payloadDA, {validSignature: false});
155
+ }
102
156
 
103
- for (const fullyVerifiedBlock of fullyVerifiedBlocks) {
104
- // TODO: Consider batching importBlock too if it takes significant time
105
- await importBlock.call(this, fullyVerifiedBlock, opts);
106
157
  await nextEventLoop();
107
158
  }
108
159
  } catch (e) {
@@ -64,6 +64,7 @@ export class PayloadEnvelopeInput {
64
64
  readonly proposerIndex: ValidatorIndex;
65
65
  readonly bid: gloas.ExecutionPayloadBid;
66
66
  readonly versionedHashes: VersionedHashes;
67
+ readonly daOutOfRange: boolean;
67
68
 
68
69
  private columnsCache = new Map<ColumnIndex, ColumnWithSource>();
69
70
 
@@ -87,6 +88,7 @@ export class PayloadEnvelopeInput {
87
88
  sampledColumns: ColumnIndex[];
88
89
  custodyColumns: ColumnIndex[];
89
90
  timeCreatedSec: number;
91
+ daOutOfRange: boolean;
90
92
  }) {
91
93
  this.blockRootHex = props.blockRootHex;
92
94
  this.slot = props.slot;
@@ -97,13 +99,14 @@ export class PayloadEnvelopeInput {
97
99
  this.sampledColumns = props.sampledColumns;
98
100
  this.custodyColumns = props.custodyColumns;
99
101
  this.timeCreatedSec = props.timeCreatedSec;
102
+ this.daOutOfRange = props.daOutOfRange;
100
103
  this.payloadEnvelopeDataPromise = createPromise();
101
104
  this.allDataPromise = createPromise();
102
105
  this.columnsDataPromise = createPromise();
103
106
 
104
107
  const noBlobs = props.bid.blobKzgCommitments.length === 0;
105
108
  const noSampledColumns = props.sampledColumns.length === 0;
106
- const hasAllData = noBlobs || noSampledColumns;
109
+ const hasAllData = props.daOutOfRange || noBlobs || noSampledColumns;
107
110
 
108
111
  if (hasAllData) {
109
112
  this.state = {hasPayload: false, hasAllData: true, hasComputedAllData: true};
@@ -125,6 +128,7 @@ export class PayloadEnvelopeInput {
125
128
  sampledColumns: props.sampledColumns,
126
129
  custodyColumns: props.custodyColumns,
127
130
  timeCreatedSec: props.timeCreatedSec,
131
+ daOutOfRange: props.daOutOfRange,
128
132
  });
129
133
  }
130
134
 
@@ -152,6 +156,7 @@ export class PayloadEnvelopeInput {
152
156
  throw new Error("Payload envelope beacon_block_root mismatch");
153
157
  }
154
158
 
159
+ // TODO GLOAS: track source by metrics, maybe inside the seen cache
155
160
  const source: SourceMeta = {
156
161
  source: props.source,
157
162
  seenTimestampSec: props.seenTimestampSec,
@@ -306,8 +311,11 @@ export class PayloadEnvelopeInput {
306
311
  return this.state.hasAllData;
307
312
  }
308
313
 
314
+ /**
315
+ * Strictly checks missing sampled columns. Does NOT short-circuit on `state.hasAllData`.
316
+ */
309
317
  getMissingSampledColumnMeta(): MissingColumnMeta {
310
- if (this.state.hasAllData) {
318
+ if (this.state.hasComputedAllData) {
311
319
  return {missing: [], versionedHashes: this.versionedHashes};
312
320
  }
313
321
 
@@ -27,6 +27,7 @@ export type CreateFromBlockProps = {
27
27
  sampledColumns: ColumnIndex[];
28
28
  custodyColumns: ColumnIndex[];
29
29
  timeCreatedSec: number;
30
+ daOutOfRange: boolean;
30
31
  };
31
32
 
32
33
  export type AddPayloadEnvelopeProps = SourceMeta & {
@@ -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} 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";
@@ -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;
@@ -107,5 +108,5 @@ export type FullyVerifiedBlock = {
107
108
  /** Seen timestamp seconds */
108
109
  seenTimestampSec: number;
109
110
  /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */
110
- executionStatus: BlockExecutionStatus;
111
+ executionStatus: BlockExecutionStatus | PayloadExecutionStatus;
111
112
  };
@@ -1,29 +1,126 @@
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
+ // 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
+ }
52
+ // Track the execution hash before the last FULL advancement so we can recover
53
+ // if the next block reveals that envelope was orphaned.
54
+ let prevExecHash: string | null = currentExecHash;
55
+ // The slot whose envelope last advanced currentExecHash (for warning context).
56
+ let lastFullSlot: Slot | null = null;
57
+
58
+ for (let i = 0; i < blocks.length; i++) {
12
59
  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});
60
+ const slot = block.message.slot;
61
+
62
+ if (i > 0) {
63
+ const prevBlock = blocks[i - 1].getBlock();
64
+ // Ensure parent root matches the previous block's root
65
+ if (
66
+ !ssz.Root.equals(
67
+ config.getForkTypes(prevBlock.message.slot).BeaconBlock.hashTreeRoot(prevBlock.message),
68
+ block.message.parentRoot
69
+ )
70
+ ) {
71
+ throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS});
72
+ }
73
+ // Ensure slots are strictly increasing
74
+ if (slot <= prevBlock.message.slot) {
75
+ throw new BlockError(block, {code: BlockErrorCode.NON_LINEAR_SLOTS});
76
+ }
23
77
  }
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});
78
+
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});
97
+ }
98
+ }
99
+ currentExecHash = prevExecHash;
100
+ }
101
+
102
+ const payloadInput = payloadEnvelopes?.get(slot) ?? null;
103
+ const payloadEnvelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : null;
104
+ if (payloadEnvelope !== null) {
105
+ // Verify the envelope references this block's root
106
+ const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message));
107
+ const envelopeBlockRoot = toRootHex(payloadEnvelope.message.beaconBlockRoot);
108
+ if (blockRoot !== envelopeBlockRoot) {
109
+ throw new BlockError(block, {
110
+ code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
111
+ envelopeBlockRoot,
112
+ blockRoot,
113
+ });
114
+ }
115
+
116
+ // FULL variant: save state before advancing, then advance
117
+ prevExecHash = currentExecHash;
118
+ lastFullSlot = slot;
119
+ currentExecHash = toRootHex(payloadEnvelope.message.payload.blockHash);
120
+ }
121
+ // EMPTY variant: currentExecHash unchanged
27
122
  }
28
123
  }
124
+
125
+ return {warnings: warnings.length > 0 ? warnings : null};
29
126
  }