@lodestar/beacon-node 1.45.0-dev.fadf0fbb1f → 1.46.0-dev.8fc4e297d2

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 (240) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +186 -57
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/index.d.ts +1 -1
  5. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  6. package/lib/api/impl/beacon/index.js.map +1 -1
  7. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  8. package/lib/api/impl/beacon/pool/index.js +4 -0
  9. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  10. package/lib/api/impl/beacon/state/index.d.ts +1 -1
  11. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  12. package/lib/api/impl/beacon/state/index.js +2 -2
  13. package/lib/api/impl/beacon/state/index.js.map +1 -1
  14. package/lib/api/impl/beacon/state/utils.d.ts +2 -1
  15. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  16. package/lib/api/impl/beacon/state/utils.js +9 -1
  17. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  18. package/lib/api/impl/debug/index.d.ts +1 -1
  19. package/lib/api/impl/debug/index.d.ts.map +1 -1
  20. package/lib/api/impl/debug/index.js +2 -2
  21. package/lib/api/impl/debug/index.js.map +1 -1
  22. package/lib/api/impl/lodestar/index.js +2 -2
  23. package/lib/api/impl/lodestar/index.js.map +1 -1
  24. package/lib/api/impl/proof/index.d.ts +1 -1
  25. package/lib/api/impl/proof/index.d.ts.map +1 -1
  26. package/lib/api/impl/proof/index.js +2 -2
  27. package/lib/api/impl/proof/index.js.map +1 -1
  28. package/lib/api/impl/utils.d.ts +23 -0
  29. package/lib/api/impl/utils.d.ts.map +1 -1
  30. package/lib/api/impl/utils.js +44 -1
  31. package/lib/api/impl/utils.js.map +1 -1
  32. package/lib/api/impl/validator/index.d.ts +0 -14
  33. package/lib/api/impl/validator/index.d.ts.map +1 -1
  34. package/lib/api/impl/validator/index.js +192 -143
  35. package/lib/api/impl/validator/index.js.map +1 -1
  36. package/lib/api/rest/base.d.ts.map +1 -1
  37. package/lib/api/rest/base.js +14 -3
  38. package/lib/api/rest/base.js.map +1 -1
  39. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  40. package/lib/chain/blocks/importBlock.js +3 -21
  41. package/lib/chain/blocks/importBlock.js.map +1 -1
  42. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  43. package/lib/chain/blocks/utils/chainSegment.js +13 -5
  44. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  45. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +1 -1
  46. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  47. package/lib/chain/blocks/verifyBlocksSanityChecks.js +1 -1
  48. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  49. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  50. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  51. package/lib/chain/builderCircuitBreaker.d.ts +31 -0
  52. package/lib/chain/builderCircuitBreaker.d.ts.map +1 -0
  53. package/lib/chain/builderCircuitBreaker.js +59 -0
  54. package/lib/chain/builderCircuitBreaker.js.map +1 -0
  55. package/lib/chain/chain.d.ts +2 -0
  56. package/lib/chain/chain.d.ts.map +1 -1
  57. package/lib/chain/chain.js +26 -2
  58. package/lib/chain/chain.js.map +1 -1
  59. package/lib/chain/errors/blockError.d.ts +48 -6
  60. package/lib/chain/errors/blockError.d.ts.map +1 -1
  61. package/lib/chain/errors/blockError.js +25 -4
  62. package/lib/chain/errors/blockError.js.map +1 -1
  63. package/lib/chain/errors/executionPayloadEnvelope.d.ts +11 -0
  64. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  65. package/lib/chain/errors/executionPayloadEnvelope.js +2 -0
  66. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  67. package/lib/chain/errors/proposerPreferences.d.ts +14 -1
  68. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  69. package/lib/chain/errors/proposerPreferences.js +2 -0
  70. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  71. package/lib/chain/interface.d.ts +2 -0
  72. package/lib/chain/interface.d.ts.map +1 -1
  73. package/lib/chain/interface.js.map +1 -1
  74. package/lib/chain/lightClient/index.d.ts.map +1 -1
  75. package/lib/chain/lightClient/index.js +11 -6
  76. package/lib/chain/lightClient/index.js.map +1 -1
  77. package/lib/chain/lightClient/proofs.d.ts.map +1 -1
  78. package/lib/chain/lightClient/proofs.js +28 -2
  79. package/lib/chain/lightClient/proofs.js.map +1 -1
  80. package/lib/chain/lightClient/types.d.ts +3 -1
  81. package/lib/chain/lightClient/types.d.ts.map +1 -1
  82. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  83. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  84. package/lib/chain/opPools/proposerPreferencesPool.d.ts +1 -1
  85. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  86. package/lib/chain/opPools/proposerPreferencesPool.js +1 -1
  87. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  88. package/lib/chain/options.d.ts +4 -0
  89. package/lib/chain/options.d.ts.map +1 -1
  90. package/lib/chain/options.js.map +1 -1
  91. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  92. package/lib/chain/prepareNextSlot.js +12 -7
  93. package/lib/chain/prepareNextSlot.js.map +1 -1
  94. package/lib/chain/validation/aggregateAndProof.js +10 -6
  95. package/lib/chain/validation/aggregateAndProof.js.map +1 -1
  96. package/lib/chain/validation/attestation.d.ts.map +1 -1
  97. package/lib/chain/validation/attestation.js +10 -6
  98. package/lib/chain/validation/attestation.js.map +1 -1
  99. package/lib/chain/validation/block.d.ts.map +1 -1
  100. package/lib/chain/validation/block.js +53 -4
  101. package/lib/chain/validation/block.js.map +1 -1
  102. package/lib/chain/validation/executionPayloadBid.js +13 -12
  103. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  104. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  105. package/lib/chain/validation/executionPayloadEnvelope.js +29 -0
  106. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  107. package/lib/chain/validation/proposerPreferences.d.ts +9 -2
  108. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  109. package/lib/chain/validation/proposerPreferences.js +64 -11
  110. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  111. package/lib/chain/validation/signatureSets/aggregateAndProof.d.ts.map +1 -1
  112. package/lib/chain/validation/signatureSets/aggregateAndProof.js +2 -3
  113. package/lib/chain/validation/signatureSets/aggregateAndProof.js.map +1 -1
  114. package/lib/db/repositories/lightclientSyncCommitteeWitness.d.ts.map +1 -1
  115. package/lib/db/repositories/lightclientSyncCommitteeWitness.js +42 -7
  116. package/lib/db/repositories/lightclientSyncCommitteeWitness.js.map +1 -1
  117. package/lib/execution/builder/http.d.ts +18 -0
  118. package/lib/execution/builder/http.d.ts.map +1 -1
  119. package/lib/execution/builder/http.js +20 -11
  120. package/lib/execution/builder/http.js.map +1 -1
  121. package/lib/metrics/metrics/beacon.d.ts +6 -0
  122. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  123. package/lib/metrics/metrics/beacon.js +18 -0
  124. package/lib/metrics/metrics/beacon.js.map +1 -1
  125. package/lib/network/core/metrics.d.ts +65 -0
  126. package/lib/network/core/metrics.d.ts.map +1 -1
  127. package/lib/network/core/metrics.js +94 -0
  128. package/lib/network/core/metrics.js.map +1 -1
  129. package/lib/network/discv5/utils.d.ts.map +1 -1
  130. package/lib/network/discv5/utils.js +12 -3
  131. package/lib/network/discv5/utils.js.map +1 -1
  132. package/lib/network/gossip/encoding.d.ts +2 -2
  133. package/lib/network/gossip/encoding.d.ts.map +1 -1
  134. package/lib/network/gossip/encoding.js +22 -9
  135. package/lib/network/gossip/encoding.js.map +1 -1
  136. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  137. package/lib/network/gossip/gossipsub.js +4 -3
  138. package/lib/network/gossip/gossipsub.js.map +1 -1
  139. package/lib/network/gossip/topic.d.ts +80 -232
  140. package/lib/network/gossip/topic.d.ts.map +1 -1
  141. package/lib/network/gossip/topic.js +47 -4
  142. package/lib/network/gossip/topic.js.map +1 -1
  143. package/lib/network/interface.d.ts +4 -1
  144. package/lib/network/interface.d.ts.map +1 -1
  145. package/lib/network/network.d.ts +4 -1
  146. package/lib/network/network.d.ts.map +1 -1
  147. package/lib/network/network.js +19 -11
  148. package/lib/network/network.js.map +1 -1
  149. package/lib/network/peers/peerManager.d.ts.map +1 -1
  150. package/lib/network/peers/peerManager.js +10 -0
  151. package/lib/network/peers/peerManager.js.map +1 -1
  152. package/lib/network/peers/score/score.d.ts +3 -1
  153. package/lib/network/peers/score/score.d.ts.map +1 -1
  154. package/lib/network/peers/score/score.js +6 -1
  155. package/lib/network/peers/score/score.js.map +1 -1
  156. package/lib/network/peers/score/store.d.ts.map +1 -1
  157. package/lib/network/peers/score/store.js +6 -1
  158. package/lib/network/peers/score/store.js.map +1 -1
  159. package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
  160. package/lib/network/peers/utils/prioritizePeers.js +14 -0
  161. package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
  162. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  163. package/lib/network/processor/gossipHandlers.js +16 -6
  164. package/lib/network/processor/gossipHandlers.js.map +1 -1
  165. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  166. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +4 -2
  167. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  168. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  169. package/lib/network/reqresp/handlers/index.js +20 -11
  170. package/lib/network/reqresp/handlers/index.js.map +1 -1
  171. package/lib/network/util.d.ts +1 -0
  172. package/lib/network/util.d.ts.map +1 -1
  173. package/lib/network/util.js +4 -0
  174. package/lib/network/util.js.map +1 -1
  175. package/lib/sync/sync.d.ts.map +1 -1
  176. package/lib/sync/sync.js +7 -0
  177. package/lib/sync/sync.js.map +1 -1
  178. package/lib/sync/unknownBlock.d.ts.map +1 -1
  179. package/lib/sync/unknownBlock.js +7 -2
  180. package/lib/sync/unknownBlock.js.map +1 -1
  181. package/lib/util/execution.js +1 -1
  182. package/lib/util/execution.js.map +1 -1
  183. package/package.json +23 -21
  184. package/src/api/impl/beacon/blocks/index.ts +221 -63
  185. package/src/api/impl/beacon/index.ts +1 -1
  186. package/src/api/impl/beacon/pool/index.ts +5 -0
  187. package/src/api/impl/beacon/state/index.ts +3 -2
  188. package/src/api/impl/beacon/state/utils.ts +11 -0
  189. package/src/api/impl/debug/index.ts +3 -2
  190. package/src/api/impl/lodestar/index.ts +2 -2
  191. package/src/api/impl/proof/index.ts +2 -2
  192. package/src/api/impl/utils.ts +51 -1
  193. package/src/api/impl/validator/index.ts +224 -157
  194. package/src/api/rest/base.ts +17 -3
  195. package/src/chain/blocks/importBlock.ts +1 -23
  196. package/src/chain/blocks/utils/chainSegment.ts +16 -5
  197. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +1 -1
  198. package/src/chain/blocks/verifyBlocksSanityChecks.ts +1 -1
  199. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +2 -2
  200. package/src/chain/builderCircuitBreaker.ts +84 -0
  201. package/src/chain/chain.ts +38 -2
  202. package/src/chain/errors/blockError.ts +44 -7
  203. package/src/chain/errors/executionPayloadEnvelope.ts +9 -0
  204. package/src/chain/errors/proposerPreferences.ts +16 -1
  205. package/src/chain/interface.ts +2 -0
  206. package/src/chain/lightClient/index.ts +12 -7
  207. package/src/chain/lightClient/proofs.ts +36 -1
  208. package/src/chain/lightClient/types.ts +3 -1
  209. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  210. package/src/chain/opPools/proposerPreferencesPool.ts +1 -1
  211. package/src/chain/options.ts +4 -0
  212. package/src/chain/prepareNextSlot.ts +11 -7
  213. package/src/chain/validation/aggregateAndProof.ts +12 -6
  214. package/src/chain/validation/attestation.ts +12 -6
  215. package/src/chain/validation/block.ts +69 -4
  216. package/src/chain/validation/executionPayloadBid.ts +16 -11
  217. package/src/chain/validation/executionPayloadEnvelope.ts +37 -0
  218. package/src/chain/validation/proposerPreferences.ts +74 -11
  219. package/src/chain/validation/signatureSets/aggregateAndProof.ts +3 -3
  220. package/src/db/repositories/lightclientSyncCommitteeWitness.ts +57 -9
  221. package/src/execution/builder/http.ts +30 -17
  222. package/src/metrics/metrics/beacon.ts +19 -0
  223. package/src/network/core/metrics.ts +94 -0
  224. package/src/network/discv5/utils.ts +11 -3
  225. package/src/network/gossip/encoding.ts +20 -7
  226. package/src/network/gossip/gossipsub.ts +9 -3
  227. package/src/network/gossip/topic.ts +58 -4
  228. package/src/network/interface.ts +3 -1
  229. package/src/network/network.ts +24 -15
  230. package/src/network/peers/peerManager.ts +11 -0
  231. package/src/network/peers/score/score.ts +8 -1
  232. package/src/network/peers/score/store.ts +8 -1
  233. package/src/network/peers/utils/prioritizePeers.ts +15 -0
  234. package/src/network/processor/gossipHandlers.ts +17 -6
  235. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +4 -2
  236. package/src/network/reqresp/handlers/index.ts +21 -12
  237. package/src/network/util.ts +5 -0
  238. package/src/sync/sync.ts +10 -0
  239. package/src/sync/unknownBlock.ts +8 -2
  240. package/src/util/execution.ts +1 -1
@@ -50,6 +50,7 @@ import {
50
50
  ssz,
51
51
  } from "@lodestar/types";
52
52
  import {
53
+ GWEI_TO_WEI,
53
54
  TimeoutError,
54
55
  defer,
55
56
  formatWeiToEth,
@@ -82,7 +83,6 @@ import {ZERO_HASH} from "../../../constants/index.js";
82
83
  import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js";
83
84
  import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js";
84
85
  import {CommitteeSubscription} from "../../../network/subnets/index.js";
85
- import {SyncState} from "../../../sync/index.js";
86
86
  import {callInNextEventLoop} from "../../../util/eventLoop.js";
87
87
  import {isOptimisticBlock} from "../../../util/forkChoice.js";
88
88
  import {getBlockGraffiti, toGraffitiBytes} from "../../../util/graffiti.js";
@@ -91,23 +91,9 @@ import {ApiOptions} from "../../options.js";
91
91
  import {getStateResponseWithRegen} from "../beacon/state/utils.js";
92
92
  import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js";
93
93
  import {ApiModules} from "../types.js";
94
+ import {notWhileSyncing} from "../utils.js";
94
95
  import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js";
95
96
 
96
- /**
97
- * If the node is within this many epochs from the head, we declare it to be synced regardless of
98
- * the network sync state.
99
- *
100
- * This helps prevent attacks where nodes can convince us that we're syncing some non-existent
101
- * finalized head.
102
- *
103
- * TODO: Lighthouse uses 8 for the attack described above. However, 8 kills Lodestar since validators
104
- * can trigger regen to fast-forward head state 8 epochs to be immediately invalidated as sync sets
105
- * a new head. Then the checkpoint state cache grows unbounded with very different states (because
106
- * they are 8 epochs apart) and causes an OOM. Research a proper solution once regen and the state
107
- * caches are better.
108
- */
109
- export const SYNC_TOLERANCE_EPOCHS = 1;
110
-
111
97
  /**
112
98
  * Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
113
99
  * Post this time, race execution and builder to pick whatever resolves first.
@@ -116,7 +102,7 @@ export const SYNC_TOLERANCE_EPOCHS = 1;
116
102
  * A cutoff of 2 seconds gives enough time and if there are unexpected delays it ensures we publish
117
103
  * in time as proposals post 4 seconds into the slot will likely be orphaned due to proposer boost reorg.
118
104
  *
119
- * TODO GLOAS: re-evaluate cutoff timing
105
+ * TODO GLOAS: re-evaluate cutoff timing due to attestation deadline changes in gloas
120
106
  */
121
107
  const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000;
122
108
  /** Overall timeout for execution and block production apis */
@@ -341,36 +327,6 @@ export function getValidatorApi(
341
327
  return null;
342
328
  }
343
329
 
344
- /**
345
- * Reject any request while the node is syncing
346
- */
347
- function notWhileSyncing(): void {
348
- // Consider node synced before or close to genesis
349
- if (chain.clock.currentSlot < SLOTS_PER_EPOCH) {
350
- return;
351
- }
352
-
353
- const syncState = sync.state;
354
- switch (syncState) {
355
- case SyncState.SyncingFinalized:
356
- case SyncState.SyncingHead: {
357
- const currentSlot = chain.clock.currentSlot;
358
- const headSlot = chain.forkChoice.getHead().slot;
359
- if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) {
360
- throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`);
361
- }
362
-
363
- return;
364
- }
365
-
366
- case SyncState.Synced:
367
- return;
368
-
369
- case SyncState.Stalled:
370
- throw new NodeIsSyncing("waiting for peers");
371
- }
372
- }
373
-
374
330
  /**
375
331
  * Post merge, the CL and EL could be out of step in the sync, and could result in
376
332
  * Syncing status of the chain head. To be precise:
@@ -573,7 +529,17 @@ export function getValidatorApi(
573
529
  builderBoostFactor?: bigint,
574
530
  {feeRecipient, builderSelection, strictFeeRecipientCheck}: routes.validator.ExtraProduceBlockOpts = {}
575
531
  ): Promise<ProduceBlindedBlockOrBlockContentsRes> {
576
- notWhileSyncing();
532
+ builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
533
+ if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
534
+ logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
535
+ builderSelection = routes.validator.BuilderSelection.BuilderAlways;
536
+ }
537
+ builderBoostFactor = builderBoostFactor ?? BigInt(100);
538
+ if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
539
+ throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
540
+ }
541
+
542
+ notWhileSyncing(chain, sync.state);
577
543
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
578
544
 
579
545
  const parentBlock = chain.getProposerHead(slot);
@@ -583,36 +549,12 @@ export function getValidatorApi(
583
549
  metrics?.blockProductionSlotDelta.set(slot - parentSlot);
584
550
 
585
551
  const fork = config.getForkName(slot);
586
- // set some sensible opts
587
- // builderSelection will be deprecated and will run in mode MaxProfit if builder is enabled
588
- // and the actual selection will be determined using builderBoostFactor passed by the validator
589
- builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
590
- builderBoostFactor = builderBoostFactor ?? BigInt(100);
591
- if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
592
- throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
593
- }
594
552
 
595
553
  const isBuilderEnabled =
596
554
  ForkSeq[fork] >= ForkSeq.bellatrix &&
597
555
  chain.executionBuilder !== undefined &&
598
556
  builderSelection !== routes.validator.BuilderSelection.ExecutionOnly;
599
557
 
600
- // At any point either the builder or execution or both flows should be active.
601
- //
602
- // Ideally such a scenario should be prevented on startup, but proposerSettingsFile or keymanager
603
- // configurations could cause a validator pubkey to have builder disabled with builder selection builder only
604
- // (TODO: independently make sure such an options update is not successful for a validator pubkey)
605
- //
606
- // So if builder is disabled ignore builder selection of builder only if caused by user mistake
607
- // https://github.com/ChainSafe/lodestar/issues/6338
608
- const isEngineEnabled = !isBuilderEnabled || builderSelection !== routes.validator.BuilderSelection.BuilderOnly;
609
-
610
- if (!isEngineEnabled && !isBuilderEnabled) {
611
- throw Error(
612
- `Internal Error: Neither builder nor execution proposal flow activated isBuilderEnabled=${isBuilderEnabled} builderSelection=${builderSelection}`
613
- );
614
- }
615
-
616
558
  const graffitiBytes = toGraffitiBytes(
617
559
  getBlockGraffiti(graffiti, getLodestarClientVersion(opts), chain.executionEngine.clientVersion, {
618
560
  private: opts.private,
@@ -627,7 +569,6 @@ export function getValidatorApi(
627
569
  fork,
628
570
  builderSelection,
629
571
  isBuilderEnabled,
630
- isEngineEnabled,
631
572
  strictFeeRecipientCheck,
632
573
  // winston logger doesn't like bigint
633
574
  builderBoostFactor: `${builderBoostFactor}`,
@@ -653,27 +594,25 @@ export function getValidatorApi(
653
594
  })
654
595
  : Promise.reject(new Error("Builder disabled"));
655
596
 
656
- const enginePromise = isEngineEnabled
657
- ? produceEngineBlockContents(slot, randaoReveal, graffitiBytes, {
658
- feeRecipient,
659
- strictFeeRecipientCheck,
660
- commonBlockBodyPromise,
661
- parentBlock,
662
- }).then((engineBlock) => {
663
- // Once the engine returns a block, in the event of either:
664
- // - suspected builder censorship
665
- // - builder boost factor set to 0 or builder selection `executionalways`
666
- // we don't need to wait for builder block as engine block will always be selected
667
- if (
668
- engineBlock.shouldOverrideBuilder ||
669
- builderBoostFactor === BigInt(0) ||
670
- builderSelection === routes.validator.BuilderSelection.ExecutionAlways
671
- ) {
672
- controller.abort();
673
- }
674
- return engineBlock;
675
- })
676
- : Promise.reject(new Error("Engine disabled"));
597
+ const enginePromise = produceEngineBlockContents(slot, randaoReveal, graffitiBytes, {
598
+ feeRecipient,
599
+ strictFeeRecipientCheck,
600
+ commonBlockBodyPromise,
601
+ parentBlock,
602
+ }).then((engineBlock) => {
603
+ // Once the engine returns a block, in the event of either:
604
+ // - suspected builder censorship
605
+ // - builder boost factor set to 0 or builder selection `executionalways`
606
+ // we don't need to wait for builder block as engine block will always be selected
607
+ if (
608
+ engineBlock.shouldOverrideBuilder ||
609
+ builderBoostFactor === BigInt(0) ||
610
+ builderSelection === routes.validator.BuilderSelection.ExecutionAlways
611
+ ) {
612
+ controller.abort();
613
+ }
614
+ return engineBlock;
615
+ });
677
616
 
678
617
  // Calculate cutoff time based on start of the slot
679
618
  const cutoffMs = Math.max(0, BLOCK_PRODUCTION_RACE_CUTOFF_MS - chain.clock.msFromSlot(slot));
@@ -720,30 +659,24 @@ export function getValidatorApi(
720
659
  throw Error("Builder and engine both failed to produce the block within timeout");
721
660
  }
722
661
 
723
- if (builder.status === "pending" && !isEngineEnabled) {
724
- throw Error("Builder failed to produce the block within timeout");
725
- }
726
-
727
662
  if (engine.status === "pending" && !isBuilderEnabled) {
728
663
  throw Error("Engine failed to produce the block within timeout");
729
664
  }
730
665
 
731
- if (isEngineEnabled) {
732
- if (engine.status === "rejected") {
733
- logger.warn(
734
- "Engine failed to produce the block",
735
- {
736
- ...loggerContext,
737
- durationMs: engine.durationMs,
738
- },
739
- engine.reason
740
- );
741
- } else if (engine.status === "pending") {
742
- logger.warn("Engine failed to produce the block within cutoff time", {
666
+ if (engine.status === "rejected") {
667
+ logger.warn(
668
+ "Engine failed to produce the block",
669
+ {
743
670
  ...loggerContext,
744
- cutoffMs,
745
- });
746
- }
671
+ durationMs: engine.durationMs,
672
+ },
673
+ engine.reason
674
+ );
675
+ } else if (engine.status === "pending") {
676
+ logger.warn("Engine failed to produce the block within cutoff time", {
677
+ ...loggerContext,
678
+ cutoffMs,
679
+ });
747
680
  }
748
681
 
749
682
  if (isBuilderEnabled) {
@@ -772,14 +705,12 @@ export function getValidatorApi(
772
705
  }
773
706
 
774
707
  if (builder.status === "rejected" && engine.status === "rejected") {
775
- throw Error(
776
- `${isBuilderEnabled && isEngineEnabled ? "Builder and engine both" : isBuilderEnabled ? "Builder" : "Engine"} failed to produce the block`
777
- );
708
+ throw Error(`${isBuilderEnabled ? "Builder and engine both" : "Engine"} failed to produce the block`);
778
709
  }
779
710
 
780
711
  // handle shouldOverrideBuilder separately
781
- if (engine.status === "fulfilled" && engine.value.shouldOverrideBuilder) {
782
- logger.info("Selected engine block: censorship suspected in builder blocks", {
712
+ if (engine.status === "fulfilled" && engine.value.shouldOverrideBuilder && isBuilderEnabled) {
713
+ logger.warn("Selected engine block: censorship suspected in builder blocks", {
783
714
  ...loggerContext,
784
715
  durationMs: engine.durationMs,
785
716
  shouldOverrideBuilder: engine.value.shouldOverrideBuilder,
@@ -796,11 +727,9 @@ export function getValidatorApi(
796
727
 
797
728
  if (builder.status === "fulfilled" && engine.status !== "fulfilled") {
798
729
  const reason =
799
- isEngineEnabled === false
800
- ? BuilderBlockSelectionReason.EngineDisabled
801
- : engine.status === "pending"
802
- ? BuilderBlockSelectionReason.EnginePending
803
- : BuilderBlockSelectionReason.EngineError;
730
+ engine.status === "pending"
731
+ ? BuilderBlockSelectionReason.EnginePending
732
+ : BuilderBlockSelectionReason.EngineError;
804
733
 
805
734
  logger.info("Selected builder block: no engine block produced", {
806
735
  reason,
@@ -910,14 +839,32 @@ export function getValidatorApi(
910
839
  return {data, meta};
911
840
  },
912
841
 
913
- async produceBlockV4({slot, randaoReveal, graffiti, feeRecipient}) {
842
+ async produceBlockV4({
843
+ slot,
844
+ randaoReveal,
845
+ graffiti,
846
+ feeRecipient,
847
+ includePayload,
848
+ builderSelection,
849
+ builderBoostFactor,
850
+ }) {
914
851
  const fork = config.getForkName(slot);
915
852
 
916
853
  if (!isForkPostGloas(fork)) {
917
854
  throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
918
855
  }
919
856
 
920
- notWhileSyncing();
857
+ builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
858
+ if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
859
+ logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
860
+ builderSelection = routes.validator.BuilderSelection.BuilderAlways;
861
+ }
862
+ builderBoostFactor = builderBoostFactor ?? BigInt(100);
863
+ if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
864
+ throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
865
+ }
866
+
867
+ notWhileSyncing(chain, sync.state);
921
868
  await waitForSlot(slot);
922
869
 
923
870
  const parentBlock = chain.getProposerHead(slot);
@@ -933,12 +880,17 @@ export function getValidatorApi(
933
880
  })
934
881
  );
935
882
 
936
- // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
937
- // the user control bid source preferences and value comparison. Also add external builder api
938
- // support when it is implemented.
883
+ // TODO GLOAS: add external builder api support when it is implemented
939
884
  const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
940
885
  const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
941
- const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
886
+ // Bids are only skipped entirely with executiononly or while the circuit breaker is active,
887
+ // other engine-preferring selections still build a block with the best bid as fallback in
888
+ // case local production fails
889
+ const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
890
+ const builderBid =
891
+ builderSelection === routes.validator.BuilderSelection.ExecutionOnly || circuitBreakerActive
892
+ ? null
893
+ : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
942
894
 
943
895
  const logCtx = {
944
896
  slot,
@@ -946,6 +898,9 @@ export function getValidatorApi(
946
898
  parentBlockRoot: parentBlockRootHex,
947
899
  parentBlockHash: parentBlock.executionPayloadBlockHash,
948
900
  fork,
901
+ builderSelection,
902
+ builderBoostFactor,
903
+ circuitBreakerActive,
949
904
  ...(builderBid !== null
950
905
  ? {
951
906
  bidValue: builderBid.message.value,
@@ -981,33 +936,101 @@ export function getValidatorApi(
981
936
  return fn().finally(() => t?.({source}));
982
937
  };
983
938
 
984
- // Always build local block. If builder bid available, also build with it in parallel and prefer it.
985
- const [engineResult, bidResult] = await Promise.allSettled([
986
- timed(ProducedBlockSource.engine, () => chain.produceBlock(baseAttrs)),
939
+ // Calculate cutoff time based on start of the slot, ensures a slow local payload build does
940
+ // not delay the proposal when a builder bid block is available (and vice versa)
941
+ const cutoffMs = Math.max(0, BLOCK_PRODUCTION_RACE_CUTOFF_MS - chain.clock.msFromSlot(slot));
942
+
943
+ // use abort controller to stop waiting for the bid block if the engine block will be selected
944
+ const controller = new AbortController();
945
+
946
+ const enginePromise: ReturnType<typeof chain.produceBlock> = timed(ProducedBlockSource.engine, () =>
947
+ chain.produceBlock(baseAttrs)
948
+ ).then((engineBlock) => {
949
+ // No need to wait for the bid block if the engine block will always be selected due to
950
+ // suspected builder censorship, a builder boost factor of 0 or executionalways selection
951
+ if (
952
+ engineBlock.shouldOverrideBuilder ||
953
+ builderBoostFactor === BigInt(0) ||
954
+ builderSelection === routes.validator.BuilderSelection.ExecutionAlways
955
+ ) {
956
+ controller.abort();
957
+ }
958
+ return engineBlock;
959
+ });
960
+ const bidPromise: ReturnType<typeof chain.produceBlock> =
987
961
  builderBid !== null
988
962
  ? timed(ProducedBlockSource.builder, () => chain.produceBlock({...baseAttrs, builderBid}))
989
- : Promise.reject(),
990
- ]);
963
+ : Promise.reject(new Error("No builder bid available"));
964
+
965
+ const [engineResult, bidResult] = await resolveOrRacePromises([enginePromise, bidPromise], {
966
+ resolveTimeoutMs: cutoffMs,
967
+ raceTimeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
968
+ signal: controller.signal,
969
+ });
991
970
 
992
971
  let bestResult: typeof engineResult | null = null;
993
972
  let source: ProducedBlockSource = ProducedBlockSource.engine;
994
- if (builderBid !== null && bidResult.status === "fulfilled") {
973
+
974
+ // handle shouldOverrideBuilder separately
975
+ if (engineResult.status === "fulfilled" && engineResult.value.shouldOverrideBuilder && builderBid !== null) {
976
+ source = ProducedBlockSource.engine;
977
+ bestResult = engineResult;
978
+ metrics?.blockProductionSelectionResults.inc({
979
+ source: ProducedBlockSource.engine,
980
+ reason: EngineBlockSelectionReason.BuilderCensorship,
981
+ });
982
+ logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
983
+ } else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
984
+ const result = selectBlockProductionSource({
985
+ builderSelection,
986
+ builderBoostFactor,
987
+ engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
988
+ // The bid value is the payment to the proposer, in Gwei
989
+ builderExecutionPayloadValue: BigInt(builderBid?.message.value ?? 0) * GWEI_TO_WEI,
990
+ });
991
+ source = result.source;
992
+ metrics?.blockProductionSelectionResults.inc(result);
993
+ logger.info(`Selected ${source} block`, {reason: result.reason, ...logCtx});
994
+ bestResult = source === ProducedBlockSource.builder ? bidResult : engineResult;
995
+ } else if (bidResult.status === "fulfilled") {
995
996
  source = ProducedBlockSource.builder;
996
997
  bestResult = bidResult;
997
- logger.info("Selected builder bid block", logCtx);
998
+ const reason =
999
+ engineResult.status === "pending"
1000
+ ? BuilderBlockSelectionReason.EnginePending
1001
+ : BuilderBlockSelectionReason.EngineError;
1002
+ metrics?.blockProductionSelectionResults.inc({source: ProducedBlockSource.builder, reason});
1003
+ logger.info("Selected builder bid block: no local block produced", {
1004
+ reason,
1005
+ ...logCtx,
1006
+ error: engineResult.status === "rejected" ? (engineResult.reason as Error).message : undefined,
1007
+ });
998
1008
  } else if (engineResult.status === "fulfilled") {
999
1009
  source = ProducedBlockSource.engine;
1000
1010
  bestResult = engineResult;
1001
- if (builderBid !== null) {
1002
- logger.warn("Builder bid block production failed, using local block", logCtx);
1003
- }
1011
+ const reason =
1012
+ builderBid === null
1013
+ ? EngineBlockSelectionReason.BuilderNoBid
1014
+ : bidResult.status === "pending"
1015
+ ? EngineBlockSelectionReason.BuilderPending
1016
+ : EngineBlockSelectionReason.BuilderError;
1017
+ metrics?.blockProductionSelectionResults.inc({source: ProducedBlockSource.engine, reason});
1018
+ logger.info("Selected local block: no builder bid block produced", {
1019
+ reason,
1020
+ ...logCtx,
1021
+ error: bidResult.status === "rejected" ? (bidResult.reason as Error).message : undefined,
1022
+ });
1004
1023
  }
1005
1024
 
1006
1025
  if (bestResult === null || bestResult.status !== "fulfilled") {
1007
- const engineReason = engineResult.status === "rejected" ? engineResult.reason : undefined;
1008
- const bidReason = builderBid !== null && bidResult.status === "rejected" ? bidResult.reason : undefined;
1009
- logger.error("Block production failed", {...logCtx, engineReason, bidReason});
1010
- throw Error(`Block production failed: engine=${engineReason ?? "n/a"} builder=${bidReason ?? "n/a"}`);
1026
+ const engineReason = engineResult.status === "rejected" ? engineResult.reason : engineResult.status;
1027
+ const bidReason = bidResult.status === "rejected" ? bidResult.reason : bidResult.status;
1028
+ logger.error("Block production failed", {
1029
+ ...logCtx,
1030
+ engineReason: String(engineReason),
1031
+ bidReason: String(bidReason),
1032
+ });
1033
+ throw Error(`Block production failed: engine=${String(engineReason)} builder=${String(bidReason)}`);
1011
1034
  }
1012
1035
 
1013
1036
  const {block, executionPayloadValue, consensusBlockValue} = bestResult.value;
@@ -1028,14 +1051,46 @@ export function getValidatorApi(
1028
1051
  void chain.persistBlock(block, "produced_engine_block");
1029
1052
  }
1030
1053
 
1054
+ // Include the payload for self-builds unless disabled (stateless flow)
1055
+ const isSelfBuild = source === ProducedBlockSource.engine;
1056
+ if (isSelfBuild && includePayload) {
1057
+ const produceResult = chain.blockProductionCache.get(blockRoot);
1058
+ if (
1059
+ produceResult === undefined ||
1060
+ !isForkPostGloas(produceResult.fork) ||
1061
+ produceResult.type !== BlockType.Full
1062
+ ) {
1063
+ throw Error(`Missing cached block production result for produced block slot=${slot} blockRoot=${blockRoot}`);
1064
+ }
1065
+ const {executionPayload, executionRequests, blobsBundle, parentBlockRoot} = produceResult as ProduceFullGloas;
1066
+
1067
+ const blockContents: gloas.BlockContents = {
1068
+ block: block as gloas.BeaconBlock,
1069
+ executionPayloadEnvelope: {
1070
+ payload: executionPayload,
1071
+ executionRequests,
1072
+ builderIndex: BUILDER_INDEX_SELF_BUILD,
1073
+ beaconBlockRoot: fromHex(blockRoot),
1074
+ parentBeaconBlockRoot: parentBlockRoot,
1075
+ },
1076
+ kzgProofs: blobsBundle.proofs,
1077
+ blobs: blobsBundle.blobs,
1078
+ };
1079
+
1080
+ return {
1081
+ data: blockContents,
1082
+ meta: {version: fork, consensusBlockValue, executionPayloadValue, executionPayloadIncluded: true},
1083
+ };
1084
+ }
1085
+
1031
1086
  return {
1032
1087
  data: block as gloas.BeaconBlock,
1033
- meta: {version: fork, consensusBlockValue},
1088
+ meta: {version: fork, consensusBlockValue, executionPayloadValue, executionPayloadIncluded: false},
1034
1089
  };
1035
1090
  },
1036
1091
 
1037
1092
  async produceAttestationData({committeeIndex, slot}) {
1038
- notWhileSyncing();
1093
+ notWhileSyncing(chain, sync.state);
1039
1094
 
1040
1095
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
1041
1096
 
@@ -1116,7 +1171,7 @@ export function getValidatorApi(
1116
1171
  throw new ApiError(400, `producePayloadAttestationData is not supported before Gloas fork=${fork}`);
1117
1172
  }
1118
1173
 
1119
- notWhileSyncing();
1174
+ notWhileSyncing(chain, sync.state);
1120
1175
  await waitForSlot(slot);
1121
1176
 
1122
1177
  const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
@@ -1201,7 +1256,7 @@ export function getValidatorApi(
1201
1256
  },
1202
1257
 
1203
1258
  async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
1204
- notWhileSyncing();
1259
+ notWhileSyncing(chain, sync.state);
1205
1260
 
1206
1261
  const currentEpoch = currentEpochWithDisparity();
1207
1262
  const nextEpoch = currentEpoch + 1;
@@ -1248,7 +1303,7 @@ export function getValidatorApi(
1248
1303
  // requested epoch is within that range, we can use the head state at current epoch
1249
1304
  state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties);
1250
1305
  } else {
1251
- const res = await getStateResponseWithRegen(chain, startSlot);
1306
+ const res = await getStateResponseWithRegen(chain, sync, startSlot);
1252
1307
 
1253
1308
  state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state;
1254
1309
 
@@ -1342,7 +1397,7 @@ export function getValidatorApi(
1342
1397
  },
1343
1398
 
1344
1399
  async getAttesterDuties({epoch, indices}) {
1345
- notWhileSyncing();
1400
+ notWhileSyncing(chain, sync.state);
1346
1401
 
1347
1402
  if (indices.length === 0) {
1348
1403
  throw new ApiError(400, "No validator to get attester duties");
@@ -1402,7 +1457,7 @@ export function getValidatorApi(
1402
1457
  },
1403
1458
 
1404
1459
  async getPtcDuties({epoch, indices}) {
1405
- notWhileSyncing();
1460
+ notWhileSyncing(chain, sync.state);
1406
1461
 
1407
1462
  if (indices.length === 0) {
1408
1463
  throw new ApiError(400, "No validator to get PTC duties");
@@ -1464,7 +1519,7 @@ export function getValidatorApi(
1464
1519
  * @param validatorIndices an array of the validator indices for which to obtain the duties.
1465
1520
  */
1466
1521
  async getSyncCommitteeDuties({epoch, indices}) {
1467
- notWhileSyncing();
1522
+ notWhileSyncing(chain, sync.state);
1468
1523
 
1469
1524
  if (indices.length === 0) {
1470
1525
  throw new ApiError(400, "No validator to get attester duties");
@@ -1511,7 +1566,7 @@ export function getValidatorApi(
1511
1566
  },
1512
1567
 
1513
1568
  async getAggregatedAttestationV2({attestationDataRoot, slot, committeeIndex}) {
1514
- notWhileSyncing();
1569
+ notWhileSyncing(chain, sync.state);
1515
1570
 
1516
1571
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
1517
1572
 
@@ -1534,7 +1589,7 @@ export function getValidatorApi(
1534
1589
  },
1535
1590
 
1536
1591
  async publishAggregateAndProofsV2({signedAggregateAndProofs}) {
1537
- notWhileSyncing();
1592
+ notWhileSyncing(chain, sync.state);
1538
1593
 
1539
1594
  const seenTimestampSec = Date.now() / 1000;
1540
1595
  const failures: FailureList = [];
@@ -1595,7 +1650,7 @@ export function getValidatorApi(
1595
1650
  * https://github.com/ethereum/beacon-APIs/pull/137
1596
1651
  */
1597
1652
  async publishContributionAndProofs({contributionAndProofs}) {
1598
- notWhileSyncing();
1653
+ notWhileSyncing(chain, sync.state);
1599
1654
 
1600
1655
  const failures: FailureList = [];
1601
1656
 
@@ -1644,7 +1699,7 @@ export function getValidatorApi(
1644
1699
  },
1645
1700
 
1646
1701
  async prepareBeaconCommitteeSubnet({subscriptions}) {
1647
- notWhileSyncing();
1702
+ notWhileSyncing(chain, sync.state);
1648
1703
 
1649
1704
  await network.prepareBeaconCommitteeSubnets(
1650
1705
  subscriptions.map(({validatorIndex, slot, isAggregator, committeesAtSlot, committeeIndex}) => ({
@@ -1677,7 +1732,7 @@ export function getValidatorApi(
1677
1732
  * https://github.com/ethereum/beacon-APIs/pull/136
1678
1733
  */
1679
1734
  async prepareSyncCommitteeSubnets({subscriptions}) {
1680
- notWhileSyncing();
1735
+ notWhileSyncing(chain, sync.state);
1681
1736
 
1682
1737
  // A `validatorIndex` can be in multiple subnets, so compute the CommitteeSubscription with double for loop
1683
1738
  const subs: CommitteeSubscription[] = [];
@@ -1821,7 +1876,7 @@ export function getValidatorApi(
1821
1876
  throw new ApiError(400, `getExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
1822
1877
  }
1823
1878
 
1824
- notWhileSyncing();
1879
+ notWhileSyncing(chain, sync.state);
1825
1880
  await waitForSlot(slot);
1826
1881
 
1827
1882
  const blockRootHex = toRootHex(beaconBlockRoot);
@@ -1839,6 +1894,18 @@ export function getValidatorApi(
1839
1894
 
1840
1895
  const {executionPayload, executionRequests, parentBlockRoot} = produceResult as ProduceFullGloas;
1841
1896
 
1897
+ if (executionPayload === undefined) {
1898
+ // Blocks committing to a builder bid are cached as full but without payload data
1899
+ throw new ApiError(404, `No local execution payload cached for block root ${blockRootHex}`);
1900
+ }
1901
+
1902
+ if (executionPayload.slotNumber !== slot) {
1903
+ throw new ApiError(
1904
+ 404,
1905
+ `Cached execution payload is for slot=${executionPayload.slotNumber}, requested slot=${slot}`
1906
+ );
1907
+ }
1908
+
1842
1909
  const envelope: gloas.ExecutionPayloadEnvelope = {
1843
1910
  payload: executionPayload,
1844
1911
  executionRequests: executionRequests,