@lodestar/beacon-node 1.47.0-dev.931ecbec99 → 1.47.0-dev.9ba9a5ce85

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 (236) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +44 -12
  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 +21 -2
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/events/index.d.ts.map +1 -1
  8. package/lib/api/impl/events/index.js +2 -0
  9. package/lib/api/impl/events/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 +237 -49
  14. package/lib/api/impl/validator/index.js.map +1 -1
  15. package/lib/api/impl/validator/utils.js +1 -1
  16. package/lib/api/impl/validator/utils.js.map +1 -1
  17. package/lib/api/rest/base.d.ts.map +1 -1
  18. package/lib/api/rest/base.js +7 -0
  19. package/lib/api/rest/base.js.map +1 -1
  20. package/lib/chain/archiveStore/archiveStore.js +1 -1
  21. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  22. package/lib/chain/blocks/importExecutionPayload.d.ts +3 -2
  23. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  24. package/lib/chain/blocks/importExecutionPayload.js +10 -8
  25. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  26. package/lib/chain/blocks/index.d.ts.map +1 -1
  27. package/lib/chain/blocks/index.js +21 -13
  28. package/lib/chain/blocks/index.js.map +1 -1
  29. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +2 -1
  30. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  31. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +6 -2
  32. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  33. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +14 -5
  34. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  35. package/lib/chain/blocks/payloadEnvelopeInput/types.js +5 -0
  36. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -1
  37. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  38. package/lib/chain/blocks/verifyBlock.js +5 -2
  39. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  40. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +22 -12
  42. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  43. package/lib/chain/chain.d.ts +7 -3
  44. package/lib/chain/chain.d.ts.map +1 -1
  45. package/lib/chain/chain.js +16 -4
  46. package/lib/chain/chain.js.map +1 -1
  47. package/lib/chain/errors/blockError.d.ts +1 -0
  48. package/lib/chain/errors/blockError.d.ts.map +1 -1
  49. package/lib/chain/errors/blockError.js +6 -1
  50. package/lib/chain/errors/blockError.js.map +1 -1
  51. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  52. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  53. package/lib/chain/errors/executionPayloadBid.js +1 -0
  54. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  55. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  56. package/lib/chain/forkChoice/index.js +4 -0
  57. package/lib/chain/forkChoice/index.js.map +1 -1
  58. package/lib/chain/interface.d.ts +6 -3
  59. package/lib/chain/interface.d.ts.map +1 -1
  60. package/lib/chain/interface.js.map +1 -1
  61. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts +16 -0
  62. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts.map +1 -0
  63. package/lib/chain/opPools/deferredVoluntaryExitPool.js +57 -0
  64. package/lib/chain/opPools/deferredVoluntaryExitPool.js.map +1 -0
  65. package/lib/chain/opPools/executionPayloadBidPool.d.ts +7 -2
  66. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  67. package/lib/chain/opPools/executionPayloadBidPool.js +4 -4
  68. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  69. package/lib/chain/opPools/index.d.ts +2 -1
  70. package/lib/chain/opPools/index.d.ts.map +1 -1
  71. package/lib/chain/opPools/index.js +1 -0
  72. package/lib/chain/opPools/index.js.map +1 -1
  73. package/lib/chain/prepareNextSlot.d.ts +5 -0
  74. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  75. package/lib/chain/prepareNextSlot.js +47 -6
  76. package/lib/chain/prepareNextSlot.js.map +1 -1
  77. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  78. package/lib/chain/produceBlock/computeNewStateRoot.js +3 -25
  79. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  80. package/lib/chain/produceBlock/produceBlockBody.js +1 -1
  81. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  82. package/lib/chain/regen/interface.d.ts +5 -2
  83. package/lib/chain/regen/interface.d.ts.map +1 -1
  84. package/lib/chain/regen/interface.js +3 -0
  85. package/lib/chain/regen/interface.js.map +1 -1
  86. package/lib/chain/regen/queued.d.ts +2 -2
  87. package/lib/chain/regen/queued.d.ts.map +1 -1
  88. package/lib/chain/regen/queued.js +5 -5
  89. package/lib/chain/regen/queued.js.map +1 -1
  90. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +28 -1
  91. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  92. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +121 -13
  93. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  94. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +5 -1
  95. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  96. package/lib/chain/stateCache/fifoBlockStateCache.js +6 -0
  97. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  98. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +47 -5
  99. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  100. package/lib/chain/stateCache/persistentCheckpointsCache.js +117 -7
  101. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  102. package/lib/chain/stateCache/types.d.ts +3 -3
  103. package/lib/chain/stateCache/types.d.ts.map +1 -1
  104. package/lib/chain/validation/block.d.ts.map +1 -1
  105. package/lib/chain/validation/block.js +40 -10
  106. package/lib/chain/validation/block.js.map +1 -1
  107. package/lib/chain/validation/executionPayloadBid.d.ts +15 -3
  108. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  109. package/lib/chain/validation/executionPayloadBid.js +165 -10
  110. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  111. package/lib/chain/validation/voluntaryExit.d.ts +9 -1
  112. package/lib/chain/validation/voluntaryExit.d.ts.map +1 -1
  113. package/lib/chain/validation/voluntaryExit.js +46 -1
  114. package/lib/chain/validation/voluntaryExit.js.map +1 -1
  115. package/lib/execution/builder/apiClient.d.ts +70 -0
  116. package/lib/execution/builder/apiClient.d.ts.map +1 -0
  117. package/lib/execution/builder/apiClient.js +202 -0
  118. package/lib/execution/builder/apiClient.js.map +1 -0
  119. package/lib/execution/builder/validateBid.d.ts +23 -0
  120. package/lib/execution/builder/validateBid.d.ts.map +1 -0
  121. package/lib/execution/builder/validateBid.js +119 -0
  122. package/lib/execution/builder/validateBid.js.map +1 -0
  123. package/lib/metrics/metrics/lodestar.d.ts +46 -5
  124. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  125. package/lib/metrics/metrics/lodestar.js +81 -5
  126. package/lib/metrics/metrics/lodestar.js.map +1 -1
  127. package/lib/network/gossip/topic.d.ts +13 -0
  128. package/lib/network/gossip/topic.d.ts.map +1 -1
  129. package/lib/network/gossip/topic.js +37 -0
  130. package/lib/network/gossip/topic.js.map +1 -1
  131. package/lib/network/network.d.ts.map +1 -1
  132. package/lib/network/network.js +23 -43
  133. package/lib/network/network.js.map +1 -1
  134. package/lib/network/processor/constants.d.ts +7 -0
  135. package/lib/network/processor/constants.d.ts.map +1 -0
  136. package/lib/network/processor/constants.js +7 -0
  137. package/lib/network/processor/constants.js.map +1 -0
  138. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  139. package/lib/network/processor/gossipHandlers.js +6 -2
  140. package/lib/network/processor/gossipHandlers.js.map +1 -1
  141. package/lib/network/processor/gossipQueues/indexed.d.ts.map +1 -1
  142. package/lib/network/processor/gossipQueues/indexed.js +0 -1
  143. package/lib/network/processor/gossipQueues/indexed.js.map +1 -1
  144. package/lib/network/processor/index.d.ts +3 -0
  145. package/lib/network/processor/index.d.ts.map +1 -1
  146. package/lib/network/processor/index.js +52 -23
  147. package/lib/network/processor/index.js.map +1 -1
  148. package/lib/network/util.d.ts +0 -1
  149. package/lib/network/util.d.ts.map +1 -1
  150. package/lib/network/util.js +0 -4
  151. package/lib/network/util.js.map +1 -1
  152. package/lib/node/deferredVoluntaryExitPublisher.d.ts +15 -0
  153. package/lib/node/deferredVoluntaryExitPublisher.d.ts.map +1 -0
  154. package/lib/node/deferredVoluntaryExitPublisher.js +38 -0
  155. package/lib/node/deferredVoluntaryExitPublisher.js.map +1 -0
  156. package/lib/node/nodejs.d.ts.map +1 -1
  157. package/lib/node/nodejs.js +7 -0
  158. package/lib/node/nodejs.js.map +1 -1
  159. package/lib/sync/range/batch.d.ts +23 -5
  160. package/lib/sync/range/batch.d.ts.map +1 -1
  161. package/lib/sync/range/batch.js +61 -22
  162. package/lib/sync/range/batch.js.map +1 -1
  163. package/lib/sync/range/chain.d.ts +2 -1
  164. package/lib/sync/range/chain.d.ts.map +1 -1
  165. package/lib/sync/range/chain.js +32 -12
  166. package/lib/sync/range/chain.js.map +1 -1
  167. package/lib/sync/range/range.js +1 -1
  168. package/lib/sync/range/range.js.map +1 -1
  169. package/lib/sync/types.d.ts +32 -0
  170. package/lib/sync/types.d.ts.map +1 -1
  171. package/lib/sync/types.js +33 -0
  172. package/lib/sync/types.js.map +1 -1
  173. package/lib/sync/unknownBlock.d.ts +6 -2
  174. package/lib/sync/unknownBlock.d.ts.map +1 -1
  175. package/lib/sync/unknownBlock.js +143 -68
  176. package/lib/sync/unknownBlock.js.map +1 -1
  177. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  178. package/lib/sync/utils/downloadByRange.js +16 -11
  179. package/lib/sync/utils/downloadByRange.js.map +1 -1
  180. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  181. package/lib/sync/utils/downloadByRoot.js +3 -1
  182. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  183. package/package.json +34 -34
  184. package/src/api/impl/beacon/blocks/index.ts +51 -15
  185. package/src/api/impl/beacon/pool/index.ts +22 -2
  186. package/src/api/impl/events/index.ts +3 -1
  187. package/src/api/impl/lodestar/index.ts +1 -1
  188. package/src/api/impl/validator/index.ts +290 -49
  189. package/src/api/impl/validator/utils.ts +1 -1
  190. package/src/api/rest/base.ts +7 -0
  191. package/src/chain/archiveStore/archiveStore.ts +1 -1
  192. package/src/chain/blocks/importExecutionPayload.ts +12 -10
  193. package/src/chain/blocks/index.ts +25 -13
  194. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +8 -2
  195. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -4
  196. package/src/chain/blocks/verifyBlock.ts +5 -2
  197. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +25 -16
  198. package/src/chain/chain.ts +18 -2
  199. package/src/chain/errors/blockError.ts +8 -2
  200. package/src/chain/errors/executionPayloadBid.ts +6 -0
  201. package/src/chain/forkChoice/index.ts +4 -0
  202. package/src/chain/interface.ts +6 -2
  203. package/src/chain/opPools/deferredVoluntaryExitPool.ts +65 -0
  204. package/src/chain/opPools/executionPayloadBidPool.ts +13 -7
  205. package/src/chain/opPools/index.ts +2 -1
  206. package/src/chain/prepareNextSlot.ts +49 -6
  207. package/src/chain/produceBlock/computeNewStateRoot.ts +3 -38
  208. package/src/chain/produceBlock/produceBlockBody.ts +1 -1
  209. package/src/chain/regen/interface.ts +5 -2
  210. package/src/chain/regen/queued.ts +5 -5
  211. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +143 -14
  212. package/src/chain/stateCache/fifoBlockStateCache.ts +8 -1
  213. package/src/chain/stateCache/persistentCheckpointsCache.ts +133 -8
  214. package/src/chain/stateCache/types.ts +3 -3
  215. package/src/chain/validation/block.ts +43 -10
  216. package/src/chain/validation/executionPayloadBid.ts +192 -12
  217. package/src/chain/validation/voluntaryExit.ts +59 -2
  218. package/src/execution/builder/apiClient.ts +264 -0
  219. package/src/execution/builder/validateBid.ts +181 -0
  220. package/src/metrics/metrics/lodestar.ts +91 -10
  221. package/src/network/gossip/topic.ts +38 -0
  222. package/src/network/network.ts +29 -54
  223. package/src/network/processor/constants.ts +6 -0
  224. package/src/network/processor/gossipHandlers.ts +6 -2
  225. package/src/network/processor/gossipQueues/indexed.ts +0 -1
  226. package/src/network/processor/index.ts +67 -30
  227. package/src/network/util.ts +0 -5
  228. package/src/node/deferredVoluntaryExitPublisher.ts +54 -0
  229. package/src/node/nodejs.ts +8 -0
  230. package/src/sync/range/batch.ts +83 -28
  231. package/src/sync/range/chain.ts +35 -13
  232. package/src/sync/range/range.ts +1 -1
  233. package/src/sync/types.ts +33 -0
  234. package/src/sync/unknownBlock.ts +167 -73
  235. package/src/sync/utils/downloadByRange.ts +21 -12
  236. package/src/sync/utils/downloadByRoot.ts +3 -1
@@ -12,6 +12,7 @@ import {
12
12
  MAX_PROPOSER_SLASHINGS,
13
13
  MAX_VOLUNTARY_EXITS,
14
14
  MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
15
+ MIN_SEED_LOOKAHEAD,
15
16
  isForkPostBellatrix,
16
17
  isForkPostDeneb,
17
18
  isForkPostGloas,
@@ -249,26 +250,58 @@ export async function validateGossipBlock(
249
250
  // This requires execution engine integration to verify the parent block hash
250
251
  }
251
252
 
252
- // use getPreState to reload state if needed. It also checks for whether the current finalized checkpoint is an ancestor of the block.
253
- // As a result, we throw an IGNORE (whereas the spec says we should REJECT for this scenario).
253
+ // For gossip forwarding we only need the state to check the block's proposer index.
254
+ // If the state cannot be regenerated we throw an IGNORE (whereas the spec says we should REJECT for the
255
+ // finalized-ancestor scenario, which is already guarded by the parentBlock lookup above).
254
256
  // this is something we should change this in the future to make the code airtight to the spec.
255
257
  // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved).
256
258
  // [REJECT] The block's parent (defined by block.parent_root) passes validation.
257
- const blockState = await chain.regen
258
- .getPreState(block, {dontTransferCache: true}, RegenCaller.validateGossipBlock)
259
- .catch(() => {
260
- throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_BLOCK_UNKNOWN, parentRoot});
259
+ const canUseParentState = blockEpoch - computeEpochAtSlot(parentBlock.slot) <= MIN_SEED_LOOKAHEAD;
260
+
261
+ const getValidationState = async () => {
262
+ const getPreState = () =>
263
+ chain.regen.getPreState(block, {dontTransferCache: true}, RegenCaller.validateGossipBlock);
264
+ if (canUseParentState) {
265
+ try {
266
+ const parentState = await chain.regen.getState(parentBlock.stateRoot, RegenCaller.validateGossipBlock);
267
+ chain.metrics?.gossipBlock.preStateSource.inc({source: "parentState"});
268
+ return parentState;
269
+ } catch {
270
+ // if parent state is not in memory, we fall back to disk reload / dial-forward
271
+ chain.metrics?.gossipBlock.preStateSource.inc({source: "fallbackPreState"});
272
+ chain.logger.debug("Parent state not in memory, falling back to getPreState for gossip block validation", {
273
+ slot: blockSlot,
274
+ root: blockRoot,
275
+ parentSlot: parentBlock.slot,
276
+ parentRoot,
277
+ });
278
+ return getPreState();
279
+ }
280
+ }
281
+ // parent is >1 epoch behind (deep skip) → beyond proposer-lookahead range, must dial forward
282
+ chain.metrics?.gossipBlock.preStateSource.inc({source: "preState"});
283
+ chain.logger.debug("Cannot use parent state for gossip block validation, dialing forward via getPreState", {
284
+ slot: blockSlot,
285
+ root: blockRoot,
286
+ parentSlot: parentBlock.slot,
287
+ parentRoot,
261
288
  });
289
+ return getPreState();
290
+ };
291
+
292
+ const state = await getValidationState().catch(() => {
293
+ throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_BLOCK_UNKNOWN, parentRoot});
294
+ });
262
295
 
263
296
  // in forky condition, make sure to populate ShufflingCache with regened state
264
- chain.shufflingCache.processState(blockState);
297
+ chain.shufflingCache.processState(state);
265
298
 
266
299
  // [REJECT] The block's execution payload timestamp is correct with respect to the slot
267
300
  // -- i.e. execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot).
268
301
  if (isForkPostBellatrix(fork) && !isForkPostGloas(fork)) {
269
302
  if (!isExecutionBlockBodyType(block.body)) throw Error("Not execution block body type");
270
303
  const executionPayload = block.body.executionPayload;
271
- if (isStatePostBellatrix(blockState) && blockState.isExecutionStateType && blockState.isExecutionEnabled(block)) {
304
+ if (isStatePostBellatrix(state) && state.isExecutionStateType && state.isExecutionEnabled(block)) {
272
305
  const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime);
273
306
  if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, chain.genesisTime)) {
274
307
  throw new BlockGossipError(GossipAction.REJECT, {
@@ -281,7 +314,7 @@ export async function validateGossipBlock(
281
314
  }
282
315
 
283
316
  // [REJECT] The proposer index is a valid validator index
284
- if (proposerIndex >= blockState.validatorCount) {
317
+ if (proposerIndex >= state.validatorCount) {
285
318
  throw new BlockGossipError(GossipAction.REJECT, {code: BlockErrorCode.UNKNOWN_PROPOSER, proposerIndex});
286
319
  }
287
320
 
@@ -293,7 +326,7 @@ export async function validateGossipBlock(
293
326
  // shuffling (defined by parent_root/slot). If the proposer_index cannot immediately be verified against the expected
294
327
  // shuffling, the block MAY be queued for later processing while proposers for the block's branch are calculated --
295
328
  // in such a case do not REJECT, instead IGNORE this message.
296
- if (blockState.getBeaconProposer(blockSlot) !== proposerIndex) {
329
+ if (state.getBeaconProposer(blockSlot) !== proposerIndex) {
297
330
  throw new BlockGossipError(GossipAction.REJECT, {code: BlockErrorCode.INCORRECT_PROPOSER, proposerIndex});
298
331
  }
299
332
 
@@ -1,4 +1,4 @@
1
- import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
1
+ import {ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
3
3
  import {
4
4
  computeEpochAtSlot,
@@ -46,11 +46,12 @@ function getMinBidValue(currentHighestBid: number): number {
46
46
  /**
47
47
  * Check whether a bid builds on one of the paths compatible with the local head branch.
48
48
  *
49
- * Building directly on the parent is allowed for proposer-boost reorgs outside epoch boundaries.
49
+ * Building directly on the parent is allowed for proposer-boost reorgs, including at epoch
50
+ * boundaries when the head is weak enough that a reorg onto the strong parent is predicted.
50
51
  * Otherwise the bid must build on the local head's full or empty payload variant, as selected for its slot.
51
52
  */
52
53
  function isBidCompatibleWithHead(
53
- forkChoice: IForkChoice,
54
+ chain: IBeaconChain,
54
55
  head: ProtoBlock,
55
56
  bidSlot: Slot,
56
57
  bidParentBlockRoot: RootHex,
@@ -60,9 +61,13 @@ function isBidCompatibleWithHead(
60
61
  const buildsOnParentPayload = bidParentBlockHash === head.parentBlockHash;
61
62
 
62
63
  if (buildsOnParentBlock && buildsOnParentPayload) {
63
- // The spec allows this at epoch boundaries, but Lodestar does not propagate these bids because validating
64
- // them requires an epoch transition for a parent state that cannot be used for proposer-boost reorgs.
65
- return !isStartSlotOfEpoch(bidSlot);
64
+ if (!isStartSlotOfEpoch(bidSlot)) {
65
+ return true;
66
+ }
67
+ // Epoch-boundary reorgs are allowed, but only propagate the bid if its parent matches the block
68
+ // we predict a proposer would reorg onto — predictProposerHead returns the strong parent only
69
+ // when the head is weak.
70
+ return chain.predictProposerHead().blockRoot === bidParentBlockRoot;
66
71
  }
67
72
 
68
73
  if (bidParentBlockRoot !== head.blockRoot) {
@@ -71,18 +76,170 @@ function isBidCompatibleWithHead(
71
76
 
72
77
  const buildsOnHeadPayload = bidParentBlockHash === head.executionPayloadBlockHash;
73
78
 
74
- if (forkChoice.shouldBuildOnFull(head, bidSlot)) {
79
+ if (chain.forkChoice.shouldBuildOnFull(head, bidSlot)) {
75
80
  return buildsOnHeadPayload;
76
81
  }
77
82
 
78
83
  return buildsOnParentPayload;
79
84
  }
80
85
 
86
+ /**
87
+ * Validation for bids submitted via the API by the operator's own builder.
88
+ *
89
+ * Only non-transient checks are applied: slot later than parent, zero execution payment, blob
90
+ * commitment count, builder eligibility, version and balance coverage, prev_randao, and signature.
91
+ * Transient gossip rules (head compatibility, first bid per tuple, value increment, proposer
92
+ * preferences) are not applied, since those only limit forwarding of peers' messages and the
93
+ * builder may legitimately bid on a branch this node does not consider head.
94
+ * The parent-payload builder-exit rule is omitted because this endpoint only accepts bids from
95
+ * the operator's own builder.
96
+ *
97
+ * Throws on any failed check. Also throws if the bid's parent block is unknown or its state is
98
+ * unavailable, since the checks cannot be evaluated against the parent branch.
99
+ */
81
100
  export async function validateApiExecutionPayloadBid(
82
101
  chain: IBeaconChain,
83
102
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
84
- ): Promise<{proposerIndex: ValidatorIndex}> {
85
- return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
103
+ ): Promise<void> {
104
+ const bid = signedExecutionPayloadBid.message;
105
+ const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
106
+
107
+ // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
108
+ if (
109
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
110
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
111
+ ) {
112
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
113
+ code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
114
+ builderIndex: bid.builderIndex,
115
+ slot: bid.slot,
116
+ });
117
+ }
118
+
119
+ // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
120
+ // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
121
+ // proposer-preferences lookup below from a known fork-choice block.
122
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
123
+ if (parentBlock === null) {
124
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
125
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
126
+ parentBlockRoot: bidParentBlockRoot,
127
+ });
128
+ }
129
+
130
+ // [REJECT] The bid is for a higher slot than its parent block -- i.e.
131
+ // validate that `bid.slot` is greater than the slot of the block with root
132
+ // `bid.parent_block_root`.
133
+ if (bid.slot <= parentBlock.slot) {
134
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
135
+ code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
136
+ parentSlot: parentBlock.slot,
137
+ slot: bid.slot,
138
+ });
139
+ }
140
+
141
+ // [REJECT] `bid.execution_payment` is zero.
142
+ if (bid.executionPayment !== 0n) {
143
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
144
+ code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
145
+ builderIndex: bid.builderIndex,
146
+ executionPayment: bid.executionPayment,
147
+ });
148
+ }
149
+
150
+ // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
151
+ // consensus layer -- i.e. validate that
152
+ // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
153
+ const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
154
+ const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
155
+ if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
156
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
157
+ code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
158
+ blobKzgCommitmentsLen,
159
+ commitmentLimit: maxBlobsPerBlock,
160
+ });
161
+ }
162
+
163
+ const state = await chain.regen
164
+ .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateApiExecutionPayloadBid)
165
+ .catch(() => {
166
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
167
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
168
+ parentBlockRoot: bidParentBlockRoot,
169
+ });
170
+ });
171
+
172
+ if (!isStatePostGloas(state)) {
173
+ throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
174
+ }
175
+
176
+ // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
177
+ // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
178
+ // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
179
+ // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
180
+ if (bid.builderIndex >= state.getBuildersLength()) {
181
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
182
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
183
+ builderIndex: bid.builderIndex,
184
+ });
185
+ }
186
+
187
+ // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
188
+ // `is_active_builder(state, bid.builder_index)` returns `True`.
189
+ const builder = state.getBuilder(bid.builderIndex);
190
+ if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
191
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
192
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
193
+ builderIndex: bid.builderIndex,
194
+ });
195
+ }
196
+
197
+ // [REJECT] The builder version is `PAYLOAD_BUILDER_VERSION` -- i.e.
198
+ // `state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION`.
199
+ if (builder.version !== PAYLOAD_BUILDER_VERSION) {
200
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
201
+ code: ExecutionPayloadBidErrorCode.INVALID_BUILDER_VERSION,
202
+ builderIndex: bid.builderIndex,
203
+ version: builder.version,
204
+ expectedVersion: PAYLOAD_BUILDER_VERSION,
205
+ });
206
+ }
207
+
208
+ // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
209
+ // i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
210
+ if (!state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
211
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
212
+ code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH,
213
+ bidValue: bid.value,
214
+ builderBalance: builder.balance,
215
+ });
216
+ }
217
+
218
+ // [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that
219
+ // `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))`.
220
+ const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
221
+ if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
222
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
223
+ code: ExecutionPayloadBidErrorCode.INVALID_PREV_RANDAO,
224
+ builderIndex: bid.builderIndex,
225
+ bidPrevRandao: toHex(bid.prevRandao),
226
+ expectedPrevRandao: toHex(randaoMix),
227
+ });
228
+ }
229
+
230
+ // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
231
+ const signatureSet = createSingleSignatureSetFromComponents(
232
+ builder.pubkey,
233
+ getExecutionPayloadBidSigningRoot(chain.config, bid),
234
+ signedExecutionPayloadBid.signature
235
+ );
236
+ if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
237
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
238
+ code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE,
239
+ builderIndex: bid.builderIndex,
240
+ slot: bid.slot,
241
+ });
242
+ }
86
243
  }
87
244
 
88
245
  export async function validateGossipExecutionPayloadBid(
@@ -114,7 +271,7 @@ async function validateExecutionPayloadBid(
114
271
 
115
272
  // [IGNORE] The bid is compatible with the current head branch.
116
273
  const head = chain.forkChoice.getHead();
117
- if (!isBidCompatibleWithHead(chain.forkChoice, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
274
+ if (!isBidCompatibleWithHead(chain, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
118
275
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
119
276
  code: ExecutionPayloadBidErrorCode.INCOMPATIBLE_WITH_HEAD,
120
277
  slot: bid.slot,
@@ -313,11 +470,11 @@ async function validateExecutionPayloadBid(
313
470
  // increment, see https://github.com/ethereum/consensus-specs/pull/4831. This prevents spam
314
471
  // from builders submitting numerous bids with minimal value increments.
315
472
  const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, bidParentBlockHash, bidParentBlockRoot);
316
- if (bestBid !== null && bid.value < getMinBidValue(bestBid.message.value)) {
473
+ if (bestBid !== null && bid.value < getMinBidValue(bestBid.signedBid.message.value)) {
317
474
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
318
475
  code: ExecutionPayloadBidErrorCode.BID_TOO_LOW,
319
476
  bidValue: bid.value,
320
- currentHighestBid: bestBid.message.value,
477
+ currentHighestBid: bestBid.signedBid.message.value,
321
478
  });
322
479
  }
323
480
  // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
@@ -342,6 +499,29 @@ async function validateExecutionPayloadBid(
342
499
  });
343
500
  }
344
501
 
502
+ // [IGNORE] The parent's payload does not try to exit the builder
503
+ if (byteArrayEquals(bid.parentBlockHash, state.latestExecutionPayloadBid.blockHash)) {
504
+ const requests = await chain.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot).catch(() => {
505
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
506
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
507
+ parentBlockHash: bidParentBlockHash,
508
+ });
509
+ });
510
+ if (
511
+ requests.builderExits.some(
512
+ (request) =>
513
+ byteArrayEquals(request.pubkey, builder.pubkey) &&
514
+ byteArrayEquals(request.sourceAddress, builder.executionAddress)
515
+ )
516
+ ) {
517
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
518
+ code: ExecutionPayloadBidErrorCode.BUILDER_MAY_EXIT,
519
+ builderIndex: bid.builderIndex,
520
+ parentBlockRoot: bidParentBlockRoot,
521
+ });
522
+ }
523
+ }
524
+
345
525
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
346
526
  const signatureSet = createSingleSignatureSetFromComponents(
347
527
  builder.pubkey,
@@ -9,12 +9,69 @@ import {
9
9
  import {IBeaconChain} from "../index.js";
10
10
  import {RegenCaller} from "../regen/index.js";
11
11
 
12
+ export type ApiVoluntaryExitResult = {status: "published"} | {status: "deferred"; validity: VoluntaryExitValidity};
13
+
14
+ // Variants that can become valid in a future epoch without user action.
15
+ // - earlyEpoch: exit.message.epoch is in the future; valid once current epoch catches up
16
+ // - shortTimeActive: validator active < SHARD_COMMITTEE_PERIOD; valid once enough time passes
17
+ // - pendingWithdrawals: Electra; valid once pending partial withdrawals drain
18
+ // Note: VoluntaryExitValidity.inactive is intentionally excluded. It conflates
19
+ // "validator does not exist" (permanent) with "validator not yet activated"
20
+ // (transient), and cleanly classifying it requires splitting the enum variant
21
+ // upstream. Left for a future follow-up.
22
+
23
+ export function isTransientExitValidity(v: VoluntaryExitValidity): boolean {
24
+ switch (v) {
25
+ case VoluntaryExitValidity.earlyEpoch:
26
+ case VoluntaryExitValidity.shortTimeActive:
27
+ case VoluntaryExitValidity.pendingWithdrawals:
28
+ return true;
29
+ case VoluntaryExitValidity.valid:
30
+ case VoluntaryExitValidity.inactive:
31
+ case VoluntaryExitValidity.alreadyExited:
32
+ case VoluntaryExitValidity.invalidSignature:
33
+ return false;
34
+ }
35
+ }
36
+
37
+ // Comments for each call are present inside `validateVoluntaryExit`.
12
38
  export async function validateApiVoluntaryExit(
13
39
  chain: IBeaconChain,
14
40
  voluntaryExit: phase0.SignedVoluntaryExit
15
- ): Promise<void> {
41
+ ): Promise<ApiVoluntaryExitResult> {
16
42
  const prioritizeBls = true;
17
- return validateVoluntaryExit(chain, voluntaryExit, prioritizeBls);
43
+
44
+ if (
45
+ chain.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex) ||
46
+ chain.deferredVoluntaryExitPool.has(voluntaryExit.message.validatorIndex)
47
+ ) {
48
+ throw new VoluntaryExitError(GossipAction.IGNORE, {
49
+ code: VoluntaryExitErrorCode.ALREADY_EXISTS,
50
+ });
51
+ }
52
+
53
+ const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateApiVoluntaryExit);
54
+ const validity = state.getVoluntaryExitValidity(voluntaryExit, false);
55
+
56
+ if (validity !== VoluntaryExitValidity.valid && !isTransientExitValidity(validity)) {
57
+ throw new VoluntaryExitError(GossipAction.REJECT, {
58
+ code: voluntaryExitValidityToErrorCode(validity),
59
+ });
60
+ }
61
+
62
+ const signatureSet = getVoluntaryExitSignatureSet(chain.config, state, voluntaryExit);
63
+ if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) {
64
+ throw new VoluntaryExitError(GossipAction.REJECT, {
65
+ code: VoluntaryExitErrorCode.INVALID_SIGNATURE,
66
+ });
67
+ }
68
+
69
+ if (validity !== VoluntaryExitValidity.valid) {
70
+ // Transient failure — signature is good, defer
71
+ return {status: "deferred", validity};
72
+ }
73
+
74
+ return {status: "published"};
18
75
  }
19
76
 
20
77
  export async function validateGossipVoluntaryExit(