@lodestar/beacon-node 1.45.0-dev.51a1c44b27 → 1.45.0-dev.6568180f96

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 (282) 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 +6 -43
  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 +48 -5
  13. package/lib/api/impl/beacon/state/index.js.map +1 -1
  14. package/lib/api/impl/beacon/state/utils.d.ts +13 -3
  15. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  16. package/lib/api/impl/beacon/state/utils.js +48 -4
  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 +238 -149
  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 +4 -22
  41. package/lib/chain/blocks/importBlock.js.map +1 -1
  42. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -2
  43. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts.map +1 -1
  44. package/lib/chain/blocks/payloadEnvelopeProcessor.js +17 -17
  45. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  46. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  47. package/lib/chain/blocks/utils/chainSegment.js +13 -5
  48. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  49. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +1 -1
  50. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  51. package/lib/chain/blocks/verifyBlocksSanityChecks.js +1 -1
  52. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  53. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  54. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  55. package/lib/chain/builderCircuitBreaker.d.ts +31 -0
  56. package/lib/chain/builderCircuitBreaker.d.ts.map +1 -0
  57. package/lib/chain/builderCircuitBreaker.js +59 -0
  58. package/lib/chain/builderCircuitBreaker.js.map +1 -0
  59. package/lib/chain/chain.d.ts +3 -2
  60. package/lib/chain/chain.d.ts.map +1 -1
  61. package/lib/chain/chain.js +27 -5
  62. package/lib/chain/chain.js.map +1 -1
  63. package/lib/chain/errors/blockError.d.ts +48 -6
  64. package/lib/chain/errors/blockError.d.ts.map +1 -1
  65. package/lib/chain/errors/blockError.js +25 -4
  66. package/lib/chain/errors/blockError.js.map +1 -1
  67. package/lib/chain/errors/executionPayloadEnvelope.d.ts +11 -0
  68. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  69. package/lib/chain/errors/executionPayloadEnvelope.js +2 -0
  70. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  71. package/lib/chain/errors/proposerPreferences.d.ts +14 -1
  72. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  73. package/lib/chain/errors/proposerPreferences.js +2 -0
  74. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  75. package/lib/chain/interface.d.ts +3 -2
  76. package/lib/chain/interface.d.ts.map +1 -1
  77. package/lib/chain/interface.js.map +1 -1
  78. package/lib/chain/lightClient/index.d.ts.map +1 -1
  79. package/lib/chain/lightClient/index.js +11 -6
  80. package/lib/chain/lightClient/index.js.map +1 -1
  81. package/lib/chain/lightClient/proofs.d.ts.map +1 -1
  82. package/lib/chain/lightClient/proofs.js +28 -2
  83. package/lib/chain/lightClient/proofs.js.map +1 -1
  84. package/lib/chain/lightClient/types.d.ts +3 -1
  85. package/lib/chain/lightClient/types.d.ts.map +1 -1
  86. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  87. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  88. package/lib/chain/opPools/proposerPreferencesPool.d.ts +3 -2
  89. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  90. package/lib/chain/opPools/proposerPreferencesPool.js +4 -1
  91. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  92. package/lib/chain/options.d.ts +5 -0
  93. package/lib/chain/options.d.ts.map +1 -1
  94. package/lib/chain/options.js +1 -0
  95. package/lib/chain/options.js.map +1 -1
  96. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  97. package/lib/chain/prepareNextSlot.js +12 -7
  98. package/lib/chain/prepareNextSlot.js.map +1 -1
  99. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  100. package/lib/chain/produceBlock/produceBlockBody.js +4 -12
  101. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  102. package/lib/chain/seenCache/index.d.ts +0 -1
  103. package/lib/chain/seenCache/index.d.ts.map +1 -1
  104. package/lib/chain/seenCache/index.js +0 -1
  105. package/lib/chain/seenCache/index.js.map +1 -1
  106. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  107. package/lib/chain/stateCache/persistentCheckpointsCache.js +3 -2
  108. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  109. package/lib/chain/validation/aggregateAndProof.js +10 -6
  110. package/lib/chain/validation/aggregateAndProof.js.map +1 -1
  111. package/lib/chain/validation/attestation.d.ts.map +1 -1
  112. package/lib/chain/validation/attestation.js +10 -6
  113. package/lib/chain/validation/attestation.js.map +1 -1
  114. package/lib/chain/validation/block.d.ts.map +1 -1
  115. package/lib/chain/validation/block.js +53 -4
  116. package/lib/chain/validation/block.js.map +1 -1
  117. package/lib/chain/validation/executionPayloadBid.js +13 -12
  118. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  119. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  120. package/lib/chain/validation/executionPayloadEnvelope.js +29 -0
  121. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  122. package/lib/chain/validation/proposerPreferences.d.ts +9 -2
  123. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  124. package/lib/chain/validation/proposerPreferences.js +75 -14
  125. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  126. package/lib/chain/validation/signatureSets/aggregateAndProof.d.ts.map +1 -1
  127. package/lib/chain/validation/signatureSets/aggregateAndProof.js +2 -3
  128. package/lib/chain/validation/signatureSets/aggregateAndProof.js.map +1 -1
  129. package/lib/db/repositories/lightclientSyncCommitteeWitness.d.ts.map +1 -1
  130. package/lib/db/repositories/lightclientSyncCommitteeWitness.js +42 -7
  131. package/lib/db/repositories/lightclientSyncCommitteeWitness.js.map +1 -1
  132. package/lib/execution/builder/http.d.ts +18 -0
  133. package/lib/execution/builder/http.d.ts.map +1 -1
  134. package/lib/execution/builder/http.js +20 -11
  135. package/lib/execution/builder/http.js.map +1 -1
  136. package/lib/metrics/metrics/beacon.d.ts +6 -0
  137. package/lib/metrics/metrics/beacon.d.ts.map +1 -1
  138. package/lib/metrics/metrics/beacon.js +18 -0
  139. package/lib/metrics/metrics/beacon.js.map +1 -1
  140. package/lib/network/core/metrics.d.ts +65 -0
  141. package/lib/network/core/metrics.d.ts.map +1 -1
  142. package/lib/network/core/metrics.js +94 -0
  143. package/lib/network/core/metrics.js.map +1 -1
  144. package/lib/network/discv5/utils.d.ts.map +1 -1
  145. package/lib/network/discv5/utils.js +12 -3
  146. package/lib/network/discv5/utils.js.map +1 -1
  147. package/lib/network/gossip/encoding.d.ts +2 -2
  148. package/lib/network/gossip/encoding.d.ts.map +1 -1
  149. package/lib/network/gossip/encoding.js +22 -9
  150. package/lib/network/gossip/encoding.js.map +1 -1
  151. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  152. package/lib/network/gossip/gossipsub.js +4 -3
  153. package/lib/network/gossip/gossipsub.js.map +1 -1
  154. package/lib/network/gossip/topic.d.ts +831 -226
  155. package/lib/network/gossip/topic.d.ts.map +1 -1
  156. package/lib/network/gossip/topic.js +47 -4
  157. package/lib/network/gossip/topic.js.map +1 -1
  158. package/lib/network/interface.d.ts +4 -1
  159. package/lib/network/interface.d.ts.map +1 -1
  160. package/lib/network/network.d.ts +4 -1
  161. package/lib/network/network.d.ts.map +1 -1
  162. package/lib/network/network.js +19 -11
  163. package/lib/network/network.js.map +1 -1
  164. package/lib/network/peers/peerManager.d.ts.map +1 -1
  165. package/lib/network/peers/peerManager.js +10 -0
  166. package/lib/network/peers/peerManager.js.map +1 -1
  167. package/lib/network/peers/score/score.d.ts +3 -1
  168. package/lib/network/peers/score/score.d.ts.map +1 -1
  169. package/lib/network/peers/score/score.js +6 -1
  170. package/lib/network/peers/score/score.js.map +1 -1
  171. package/lib/network/peers/score/store.d.ts.map +1 -1
  172. package/lib/network/peers/score/store.js +6 -1
  173. package/lib/network/peers/score/store.js.map +1 -1
  174. package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
  175. package/lib/network/peers/utils/prioritizePeers.js +14 -0
  176. package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
  177. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  178. package/lib/network/processor/gossipHandlers.js +18 -9
  179. package/lib/network/processor/gossipHandlers.js.map +1 -1
  180. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  181. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +4 -2
  182. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  183. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  184. package/lib/network/reqresp/handlers/index.js +20 -11
  185. package/lib/network/reqresp/handlers/index.js.map +1 -1
  186. package/lib/network/util.d.ts +1 -0
  187. package/lib/network/util.d.ts.map +1 -1
  188. package/lib/network/util.js +4 -0
  189. package/lib/network/util.js.map +1 -1
  190. package/lib/sync/range/chain.d.ts +4 -1
  191. package/lib/sync/range/chain.d.ts.map +1 -1
  192. package/lib/sync/range/chain.js +3 -3
  193. package/lib/sync/range/chain.js.map +1 -1
  194. package/lib/sync/range/range.d.ts.map +1 -1
  195. package/lib/sync/range/range.js +10 -0
  196. package/lib/sync/range/range.js.map +1 -1
  197. package/lib/sync/sync.d.ts.map +1 -1
  198. package/lib/sync/sync.js +7 -0
  199. package/lib/sync/sync.js.map +1 -1
  200. package/lib/sync/unknownBlock.d.ts.map +1 -1
  201. package/lib/sync/unknownBlock.js +7 -2
  202. package/lib/sync/unknownBlock.js.map +1 -1
  203. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  204. package/lib/sync/utils/downloadByRange.js +6 -0
  205. package/lib/sync/utils/downloadByRange.js.map +1 -1
  206. package/lib/util/execution.js +1 -1
  207. package/lib/util/execution.js.map +1 -1
  208. package/lib/util/graffiti.d.ts +15 -0
  209. package/lib/util/graffiti.d.ts.map +1 -1
  210. package/lib/util/graffiti.js +55 -0
  211. package/lib/util/graffiti.js.map +1 -1
  212. package/package.json +23 -21
  213. package/src/api/impl/beacon/blocks/index.ts +221 -63
  214. package/src/api/impl/beacon/index.ts +1 -1
  215. package/src/api/impl/beacon/pool/index.ts +5 -53
  216. package/src/api/impl/beacon/state/index.ts +55 -3
  217. package/src/api/impl/beacon/state/utils.ts +68 -4
  218. package/src/api/impl/debug/index.ts +3 -2
  219. package/src/api/impl/lodestar/index.ts +2 -2
  220. package/src/api/impl/proof/index.ts +2 -2
  221. package/src/api/impl/utils.ts +51 -1
  222. package/src/api/impl/validator/index.ts +278 -162
  223. package/src/api/rest/base.ts +17 -3
  224. package/src/chain/blocks/importBlock.ts +2 -24
  225. package/src/chain/blocks/payloadEnvelopeProcessor.ts +17 -17
  226. package/src/chain/blocks/utils/chainSegment.ts +16 -5
  227. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +1 -1
  228. package/src/chain/blocks/verifyBlocksSanityChecks.ts +1 -1
  229. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +2 -2
  230. package/src/chain/builderCircuitBreaker.ts +84 -0
  231. package/src/chain/chain.ts +38 -5
  232. package/src/chain/errors/blockError.ts +44 -7
  233. package/src/chain/errors/executionPayloadEnvelope.ts +9 -0
  234. package/src/chain/errors/proposerPreferences.ts +16 -1
  235. package/src/chain/interface.ts +2 -2
  236. package/src/chain/lightClient/index.ts +12 -7
  237. package/src/chain/lightClient/proofs.ts +36 -1
  238. package/src/chain/lightClient/types.ts +3 -1
  239. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  240. package/src/chain/opPools/proposerPreferencesPool.ts +6 -2
  241. package/src/chain/options.ts +6 -0
  242. package/src/chain/prepareNextSlot.ts +11 -7
  243. package/src/chain/produceBlock/produceBlockBody.ts +9 -17
  244. package/src/chain/seenCache/index.ts +0 -1
  245. package/src/chain/stateCache/persistentCheckpointsCache.ts +3 -2
  246. package/src/chain/validation/aggregateAndProof.ts +12 -6
  247. package/src/chain/validation/attestation.ts +12 -6
  248. package/src/chain/validation/block.ts +69 -4
  249. package/src/chain/validation/executionPayloadBid.ts +16 -11
  250. package/src/chain/validation/executionPayloadEnvelope.ts +37 -0
  251. package/src/chain/validation/proposerPreferences.ts +86 -14
  252. package/src/chain/validation/signatureSets/aggregateAndProof.ts +3 -3
  253. package/src/db/repositories/lightclientSyncCommitteeWitness.ts +57 -9
  254. package/src/execution/builder/http.ts +30 -17
  255. package/src/metrics/metrics/beacon.ts +19 -0
  256. package/src/network/core/metrics.ts +94 -0
  257. package/src/network/discv5/utils.ts +11 -3
  258. package/src/network/gossip/encoding.ts +20 -7
  259. package/src/network/gossip/gossipsub.ts +9 -3
  260. package/src/network/gossip/topic.ts +58 -4
  261. package/src/network/interface.ts +3 -1
  262. package/src/network/network.ts +24 -15
  263. package/src/network/peers/peerManager.ts +11 -0
  264. package/src/network/peers/score/score.ts +8 -1
  265. package/src/network/peers/score/store.ts +8 -1
  266. package/src/network/peers/utils/prioritizePeers.ts +15 -0
  267. package/src/network/processor/gossipHandlers.ts +18 -8
  268. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +4 -2
  269. package/src/network/reqresp/handlers/index.ts +21 -12
  270. package/src/network/util.ts +5 -0
  271. package/src/sync/range/chain.ts +4 -5
  272. package/src/sync/range/range.ts +10 -0
  273. package/src/sync/sync.ts +10 -0
  274. package/src/sync/unknownBlock.ts +8 -2
  275. package/src/sync/utils/downloadByRange.ts +6 -0
  276. package/src/util/execution.ts +1 -1
  277. package/src/util/graffiti.ts +78 -0
  278. package/lib/chain/seenCache/seenProposerPreferences.d.ts +0 -16
  279. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +0 -1
  280. package/lib/chain/seenCache/seenProposerPreferences.js +0 -26
  281. package/lib/chain/seenCache/seenProposerPreferences.js.map +0 -1
  282. package/src/chain/seenCache/seenProposerPreferences.ts +0 -32
@@ -3,41 +3,28 @@ import { ExecutionStatus, PayloadStatus } from "@lodestar/fork-choice";
3
3
  import { BUILDER_INDEX_SELF_BUILD, ForkName, ForkSeq, GENESIS_SLOT, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostBellatrix, isForkPostDeneb, isForkPostElectra, isForkPostFulu, isForkPostGloas, } from "@lodestar/params";
4
4
  import { DataAvailabilityStatus, beaconBlockToBlinded, calculateCommitteeAssignments, computeEpochAtSlot, computeStartSlotAtEpoch, computeTimeAtSlot, getCurrentSlot, isStatePostAltair, isStatePostGloas, proposerShufflingDecisionRoot, } from "@lodestar/state-transition";
5
5
  import { ProducedBlockSource, getValidatorStatus, ssz, } from "@lodestar/types";
6
- import { TimeoutError, defer, formatWeiToEth, fromHex, prettyWeiToEth, resolveOrRacePromises, toHex, toRootHex, } from "@lodestar/utils";
6
+ import { GWEI_TO_WEI, TimeoutError, defer, formatWeiToEth, fromHex, prettyWeiToEth, resolveOrRacePromises, toHex, toRootHex, } from "@lodestar/utils";
7
7
  import { MAX_BUILDER_BOOST_FACTOR } from "@lodestar/validator";
8
8
  import { BlockInputSource } from "../../../chain/blocks/blockInput/types.js";
9
- import { AttestationError, AttestationErrorCode, GossipAction, SyncCommitteeError, SyncCommitteeErrorCode, } from "../../../chain/errors/index.js";
9
+ import { AttestationError, AttestationErrorCode, GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode, SyncCommitteeError, SyncCommitteeErrorCode, } from "../../../chain/errors/index.js";
10
10
  import { ChainEvent } from "../../../chain/index.js";
11
11
  import { PREPARE_NEXT_SLOT_BPS } from "../../../chain/prepareNextSlot.js";
12
12
  import { BlockType } from "../../../chain/produceBlock/index.js";
13
13
  import { RegenCaller } from "../../../chain/regen/index.js";
14
14
  import { validateApiAggregateAndProof } from "../../../chain/validation/index.js";
15
+ import { validateGossipProposerPreferences } from "../../../chain/validation/proposerPreferences.js";
15
16
  import { validateSyncCommitteeGossipContributionAndProof } from "../../../chain/validation/syncCommitteeContributionAndProof.js";
16
17
  import { ZERO_HASH } from "../../../constants/index.js";
17
18
  import { BuilderStatus, NoBidReceived } from "../../../execution/builder/http.js";
18
19
  import { validateGossipFnRetryUnknownRoot } from "../../../network/processor/gossipHandlers.js";
19
- import { SyncState } from "../../../sync/index.js";
20
20
  import { callInNextEventLoop } from "../../../util/eventLoop.js";
21
21
  import { isOptimisticBlock } from "../../../util/forkChoice.js";
22
- import { getDefaultGraffiti, toGraffitiBytes } from "../../../util/graffiti.js";
22
+ import { getBlockGraffiti, toGraffitiBytes } from "../../../util/graffiti.js";
23
23
  import { getLodestarClientVersion } from "../../../util/metadata.js";
24
24
  import { getStateResponseWithRegen } from "../beacon/state/utils.js";
25
25
  import { ApiError, IndexedError, NodeIsSyncing, OnlySupportedByDVT } from "../errors.js";
26
+ import { notWhileSyncing } from "../utils.js";
26
27
  import { computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource } from "./utils.js";
27
- /**
28
- * If the node is within this many epochs from the head, we declare it to be synced regardless of
29
- * the network sync state.
30
- *
31
- * This helps prevent attacks where nodes can convince us that we're syncing some non-existent
32
- * finalized head.
33
- *
34
- * TODO: Lighthouse uses 8 for the attack described above. However, 8 kills Lodestar since validators
35
- * can trigger regen to fast-forward head state 8 epochs to be immediately invalidated as sync sets
36
- * a new head. Then the checkpoint state cache grows unbounded with very different states (because
37
- * they are 8 epochs apart) and causes an OOM. Research a proper solution once regen and the state
38
- * caches are better.
39
- */
40
- export const SYNC_TOLERANCE_EPOCHS = 1;
41
28
  /**
42
29
  * Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
43
30
  * Post this time, race execution and builder to pick whatever resolves first.
@@ -46,7 +33,7 @@ export const SYNC_TOLERANCE_EPOCHS = 1;
46
33
  * A cutoff of 2 seconds gives enough time and if there are unexpected delays it ensures we publish
47
34
  * in time as proposals post 4 seconds into the slot will likely be orphaned due to proposer boost reorg.
48
35
  *
49
- * TODO GLOAS: re-evaluate cutoff timing
36
+ * TODO GLOAS: re-evaluate cutoff timing due to attestation deadline changes in gloas
50
37
  */
51
38
  const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000;
52
39
  /** Overall timeout for execution and block production apis */
@@ -221,31 +208,6 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
221
208
  }
222
209
  return null;
223
210
  }
224
- /**
225
- * Reject any request while the node is syncing
226
- */
227
- function notWhileSyncing() {
228
- // Consider node synced before or close to genesis
229
- if (chain.clock.currentSlot < SLOTS_PER_EPOCH) {
230
- return;
231
- }
232
- const syncState = sync.state;
233
- switch (syncState) {
234
- case SyncState.SyncingFinalized:
235
- case SyncState.SyncingHead: {
236
- const currentSlot = chain.clock.currentSlot;
237
- const headSlot = chain.forkChoice.getHead().slot;
238
- if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) {
239
- throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`);
240
- }
241
- return;
242
- }
243
- case SyncState.Synced:
244
- return;
245
- case SyncState.Stalled:
246
- throw new NodeIsSyncing("waiting for peers");
247
- }
248
- }
249
211
  /**
250
212
  * Post merge, the CL and EL could be out of step in the sync, and could result in
251
213
  * Syncing status of the chain head. To be precise:
@@ -402,7 +364,16 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
402
364
  async function produceEngineOrBuilderBlock(slot, randaoReveal, graffiti,
403
365
  // TODO deneb: skip randao verification
404
366
  _skipRandaoVerification, builderBoostFactor, { feeRecipient, builderSelection, strictFeeRecipientCheck } = {}) {
405
- notWhileSyncing();
367
+ builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
368
+ if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
369
+ logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
370
+ builderSelection = routes.validator.BuilderSelection.BuilderAlways;
371
+ }
372
+ builderBoostFactor = builderBoostFactor ?? BigInt(100);
373
+ if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
374
+ throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
375
+ }
376
+ notWhileSyncing(chain, sync.state);
406
377
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
407
378
  const parentBlock = chain.getProposerHead(slot);
408
379
  const { blockRoot: parentBlockRootHex, slot: parentSlot } = parentBlock;
@@ -410,30 +381,13 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
410
381
  notOnOutOfRangeData(parentBlockRoot);
411
382
  metrics?.blockProductionSlotDelta.set(slot - parentSlot);
412
383
  const fork = config.getForkName(slot);
413
- // set some sensible opts
414
- // builderSelection will be deprecated and will run in mode MaxProfit if builder is enabled
415
- // and the actual selection will be determined using builderBoostFactor passed by the validator
416
- builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
417
- builderBoostFactor = builderBoostFactor ?? BigInt(100);
418
- if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
419
- throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
420
- }
421
384
  const isBuilderEnabled = ForkSeq[fork] >= ForkSeq.bellatrix &&
422
385
  chain.executionBuilder !== undefined &&
423
386
  builderSelection !== routes.validator.BuilderSelection.ExecutionOnly;
424
- // At any point either the builder or execution or both flows should be active.
425
- //
426
- // Ideally such a scenario should be prevented on startup, but proposerSettingsFile or keymanager
427
- // configurations could cause a validator pubkey to have builder disabled with builder selection builder only
428
- // (TODO: independently make sure such an options update is not successful for a validator pubkey)
429
- //
430
- // So if builder is disabled ignore builder selection of builder only if caused by user mistake
431
- // https://github.com/ChainSafe/lodestar/issues/6338
432
- const isEngineEnabled = !isBuilderEnabled || builderSelection !== routes.validator.BuilderSelection.BuilderOnly;
433
- if (!isEngineEnabled && !isBuilderEnabled) {
434
- throw Error(`Internal Error: Neither builder nor execution proposal flow activated isBuilderEnabled=${isBuilderEnabled} builderSelection=${builderSelection}`);
435
- }
436
- const graffitiBytes = toGraffitiBytes(graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts));
387
+ const graffitiBytes = toGraffitiBytes(getBlockGraffiti(graffiti, getLodestarClientVersion(opts), chain.executionEngine.clientVersion, {
388
+ private: opts.private,
389
+ graffitiAppend: chain.opts.graffitiAppend,
390
+ }));
437
391
  const loggerContext = {
438
392
  slot,
439
393
  parentSlot,
@@ -441,7 +395,6 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
441
395
  fork,
442
396
  builderSelection,
443
397
  isBuilderEnabled,
444
- isEngineEnabled,
445
398
  strictFeeRecipientCheck,
446
399
  // winston logger doesn't like bigint
447
400
  builderBoostFactor: `${builderBoostFactor}`,
@@ -462,25 +415,23 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
462
415
  parentBlock,
463
416
  })
464
417
  : Promise.reject(new Error("Builder disabled"));
465
- const enginePromise = isEngineEnabled
466
- ? produceEngineBlockContents(slot, randaoReveal, graffitiBytes, {
467
- feeRecipient,
468
- strictFeeRecipientCheck,
469
- commonBlockBodyPromise,
470
- parentBlock,
471
- }).then((engineBlock) => {
472
- // Once the engine returns a block, in the event of either:
473
- // - suspected builder censorship
474
- // - builder boost factor set to 0 or builder selection `executionalways`
475
- // we don't need to wait for builder block as engine block will always be selected
476
- if (engineBlock.shouldOverrideBuilder ||
477
- builderBoostFactor === BigInt(0) ||
478
- builderSelection === routes.validator.BuilderSelection.ExecutionAlways) {
479
- controller.abort();
480
- }
481
- return engineBlock;
482
- })
483
- : Promise.reject(new Error("Engine disabled"));
418
+ const enginePromise = produceEngineBlockContents(slot, randaoReveal, graffitiBytes, {
419
+ feeRecipient,
420
+ strictFeeRecipientCheck,
421
+ commonBlockBodyPromise,
422
+ parentBlock,
423
+ }).then((engineBlock) => {
424
+ // Once the engine returns a block, in the event of either:
425
+ // - suspected builder censorship
426
+ // - builder boost factor set to 0 or builder selection `executionalways`
427
+ // we don't need to wait for builder block as engine block will always be selected
428
+ if (engineBlock.shouldOverrideBuilder ||
429
+ builderBoostFactor === BigInt(0) ||
430
+ builderSelection === routes.validator.BuilderSelection.ExecutionAlways) {
431
+ controller.abort();
432
+ }
433
+ return engineBlock;
434
+ });
484
435
  // Calculate cutoff time based on start of the slot
485
436
  const cutoffMs = Math.max(0, BLOCK_PRODUCTION_RACE_CUTOFF_MS - chain.clock.msFromSlot(slot));
486
437
  logger.verbose("Block production race (builder vs execution) starting", {
@@ -519,25 +470,20 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
519
470
  if (builder.status === "pending" && engine.status === "pending") {
520
471
  throw Error("Builder and engine both failed to produce the block within timeout");
521
472
  }
522
- if (builder.status === "pending" && !isEngineEnabled) {
523
- throw Error("Builder failed to produce the block within timeout");
524
- }
525
473
  if (engine.status === "pending" && !isBuilderEnabled) {
526
474
  throw Error("Engine failed to produce the block within timeout");
527
475
  }
528
- if (isEngineEnabled) {
529
- if (engine.status === "rejected") {
530
- logger.warn("Engine failed to produce the block", {
531
- ...loggerContext,
532
- durationMs: engine.durationMs,
533
- }, engine.reason);
534
- }
535
- else if (engine.status === "pending") {
536
- logger.warn("Engine failed to produce the block within cutoff time", {
537
- ...loggerContext,
538
- cutoffMs,
539
- });
540
- }
476
+ if (engine.status === "rejected") {
477
+ logger.warn("Engine failed to produce the block", {
478
+ ...loggerContext,
479
+ durationMs: engine.durationMs,
480
+ }, engine.reason);
481
+ }
482
+ else if (engine.status === "pending") {
483
+ logger.warn("Engine failed to produce the block within cutoff time", {
484
+ ...loggerContext,
485
+ cutoffMs,
486
+ });
541
487
  }
542
488
  if (isBuilderEnabled) {
543
489
  if (builder.status === "rejected") {
@@ -562,11 +508,11 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
562
508
  }
563
509
  }
564
510
  if (builder.status === "rejected" && engine.status === "rejected") {
565
- throw Error(`${isBuilderEnabled && isEngineEnabled ? "Builder and engine both" : isBuilderEnabled ? "Builder" : "Engine"} failed to produce the block`);
511
+ throw Error(`${isBuilderEnabled ? "Builder and engine both" : "Engine"} failed to produce the block`);
566
512
  }
567
513
  // handle shouldOverrideBuilder separately
568
- if (engine.status === "fulfilled" && engine.value.shouldOverrideBuilder) {
569
- logger.info("Selected engine block: censorship suspected in builder blocks", {
514
+ if (engine.status === "fulfilled" && engine.value.shouldOverrideBuilder && isBuilderEnabled) {
515
+ logger.warn("Selected engine block: censorship suspected in builder blocks", {
570
516
  ...loggerContext,
571
517
  durationMs: engine.durationMs,
572
518
  shouldOverrideBuilder: engine.value.shouldOverrideBuilder,
@@ -579,11 +525,9 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
579
525
  return { ...engine.value, executionPayloadBlinded: false, executionPayloadSource: ProducedBlockSource.engine };
580
526
  }
581
527
  if (builder.status === "fulfilled" && engine.status !== "fulfilled") {
582
- const reason = isEngineEnabled === false
583
- ? BuilderBlockSelectionReason.EngineDisabled
584
- : engine.status === "pending"
585
- ? BuilderBlockSelectionReason.EnginePending
586
- : BuilderBlockSelectionReason.EngineError;
528
+ const reason = engine.status === "pending"
529
+ ? BuilderBlockSelectionReason.EnginePending
530
+ : BuilderBlockSelectionReason.EngineError;
587
531
  logger.info("Selected builder block: no engine block produced", {
588
532
  reason,
589
533
  ...loggerContext,
@@ -667,31 +611,50 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
667
611
  }
668
612
  return { data, meta };
669
613
  },
670
- async produceBlockV4({ slot, randaoReveal, graffiti, feeRecipient }) {
614
+ async produceBlockV4({ slot, randaoReveal, graffiti, feeRecipient, includePayload, builderSelection, builderBoostFactor, }) {
671
615
  const fork = config.getForkName(slot);
672
616
  if (!isForkPostGloas(fork)) {
673
617
  throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
674
618
  }
675
- notWhileSyncing();
619
+ builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
620
+ if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
621
+ logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
622
+ builderSelection = routes.validator.BuilderSelection.BuilderAlways;
623
+ }
624
+ builderBoostFactor = builderBoostFactor ?? BigInt(100);
625
+ if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
626
+ throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
627
+ }
628
+ notWhileSyncing(chain, sync.state);
676
629
  await waitForSlot(slot);
677
630
  const parentBlock = chain.getProposerHead(slot);
678
631
  const { blockRoot: parentBlockRootHex, slot: parentSlot } = parentBlock;
679
632
  const parentBlockRoot = fromHex(parentBlockRootHex);
680
633
  notOnOutOfRangeData(parentBlockRoot);
681
634
  metrics?.blockProductionSlotDelta.set(slot - parentSlot);
682
- const graffitiBytes = toGraffitiBytes(graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts));
683
- // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
684
- // the user control bid source preferences and value comparison. Also add external builder api
685
- // support when it is implemented.
635
+ const graffitiBytes = toGraffitiBytes(getBlockGraffiti(graffiti, getLodestarClientVersion(opts), chain.executionEngine.clientVersion, {
636
+ private: opts.private,
637
+ graffitiAppend: chain.opts.graffitiAppend,
638
+ }));
639
+ // TODO GLOAS: add external builder api support when it is implemented
686
640
  const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
687
641
  const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
688
- const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
642
+ // Bids are only skipped entirely with executiononly or while the circuit breaker is active,
643
+ // other engine-preferring selections still build a block with the best bid as fallback in
644
+ // case local production fails
645
+ const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
646
+ const builderBid = builderSelection === routes.validator.BuilderSelection.ExecutionOnly || circuitBreakerActive
647
+ ? null
648
+ : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
689
649
  const logCtx = {
690
650
  slot,
691
651
  parentSlot,
692
652
  parentBlockRoot: parentBlockRootHex,
693
653
  parentBlockHash: parentBlock.executionPayloadBlockHash,
694
654
  fork,
655
+ builderSelection,
656
+ builderBoostFactor,
657
+ circuitBreakerActive,
695
658
  ...(builderBid !== null
696
659
  ? {
697
660
  bidValue: builderBid.message.value,
@@ -722,32 +685,91 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
722
685
  const t = metrics?.blockProductionTime.startTimer();
723
686
  return fn().finally(() => t?.({ source }));
724
687
  };
725
- // Always build local block. If builder bid available, also build with it in parallel and prefer it.
726
- const [engineResult, bidResult] = await Promise.allSettled([
727
- timed(ProducedBlockSource.engine, () => chain.produceBlock(baseAttrs)),
728
- builderBid !== null
729
- ? timed(ProducedBlockSource.builder, () => chain.produceBlock({ ...baseAttrs, builderBid }))
730
- : Promise.reject(),
731
- ]);
688
+ // Calculate cutoff time based on start of the slot, ensures a slow local payload build does
689
+ // not delay the proposal when a builder bid block is available (and vice versa)
690
+ const cutoffMs = Math.max(0, BLOCK_PRODUCTION_RACE_CUTOFF_MS - chain.clock.msFromSlot(slot));
691
+ // use abort controller to stop waiting for the bid block if the engine block will be selected
692
+ const controller = new AbortController();
693
+ const enginePromise = timed(ProducedBlockSource.engine, () => chain.produceBlock(baseAttrs)).then((engineBlock) => {
694
+ // No need to wait for the bid block if the engine block will always be selected due to
695
+ // suspected builder censorship, a builder boost factor of 0 or executionalways selection
696
+ if (engineBlock.shouldOverrideBuilder ||
697
+ builderBoostFactor === BigInt(0) ||
698
+ builderSelection === routes.validator.BuilderSelection.ExecutionAlways) {
699
+ controller.abort();
700
+ }
701
+ return engineBlock;
702
+ });
703
+ const bidPromise = builderBid !== null
704
+ ? timed(ProducedBlockSource.builder, () => chain.produceBlock({ ...baseAttrs, builderBid }))
705
+ : Promise.reject(new Error("No builder bid available"));
706
+ const [engineResult, bidResult] = await resolveOrRacePromises([enginePromise, bidPromise], {
707
+ resolveTimeoutMs: cutoffMs,
708
+ raceTimeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
709
+ signal: controller.signal,
710
+ });
732
711
  let bestResult = null;
733
712
  let source = ProducedBlockSource.engine;
734
- if (builderBid !== null && bidResult.status === "fulfilled") {
713
+ // handle shouldOverrideBuilder separately
714
+ if (engineResult.status === "fulfilled" && engineResult.value.shouldOverrideBuilder && builderBid !== null) {
715
+ source = ProducedBlockSource.engine;
716
+ bestResult = engineResult;
717
+ metrics?.blockProductionSelectionResults.inc({
718
+ source: ProducedBlockSource.engine,
719
+ reason: EngineBlockSelectionReason.BuilderCensorship,
720
+ });
721
+ logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
722
+ }
723
+ else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
724
+ const result = selectBlockProductionSource({
725
+ builderSelection,
726
+ builderBoostFactor,
727
+ engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
728
+ // The bid value is the payment to the proposer, in Gwei
729
+ builderExecutionPayloadValue: BigInt(builderBid?.message.value ?? 0) * GWEI_TO_WEI,
730
+ });
731
+ source = result.source;
732
+ metrics?.blockProductionSelectionResults.inc(result);
733
+ logger.info(`Selected ${source} block`, { reason: result.reason, ...logCtx });
734
+ bestResult = source === ProducedBlockSource.builder ? bidResult : engineResult;
735
+ }
736
+ else if (bidResult.status === "fulfilled") {
735
737
  source = ProducedBlockSource.builder;
736
738
  bestResult = bidResult;
737
- logger.info("Selected builder bid block", logCtx);
739
+ const reason = engineResult.status === "pending"
740
+ ? BuilderBlockSelectionReason.EnginePending
741
+ : BuilderBlockSelectionReason.EngineError;
742
+ metrics?.blockProductionSelectionResults.inc({ source: ProducedBlockSource.builder, reason });
743
+ logger.info("Selected builder bid block: no local block produced", {
744
+ reason,
745
+ ...logCtx,
746
+ error: engineResult.status === "rejected" ? engineResult.reason.message : undefined,
747
+ });
738
748
  }
739
749
  else if (engineResult.status === "fulfilled") {
740
750
  source = ProducedBlockSource.engine;
741
751
  bestResult = engineResult;
742
- if (builderBid !== null) {
743
- logger.warn("Builder bid block production failed, using local block", logCtx);
744
- }
752
+ const reason = builderBid === null
753
+ ? EngineBlockSelectionReason.BuilderNoBid
754
+ : bidResult.status === "pending"
755
+ ? EngineBlockSelectionReason.BuilderPending
756
+ : EngineBlockSelectionReason.BuilderError;
757
+ metrics?.blockProductionSelectionResults.inc({ source: ProducedBlockSource.engine, reason });
758
+ logger.info("Selected local block: no builder bid block produced", {
759
+ reason,
760
+ ...logCtx,
761
+ error: bidResult.status === "rejected" ? bidResult.reason.message : undefined,
762
+ });
745
763
  }
746
764
  if (bestResult === null || bestResult.status !== "fulfilled") {
747
- const engineReason = engineResult.status === "rejected" ? engineResult.reason : undefined;
748
- const bidReason = builderBid !== null && bidResult.status === "rejected" ? bidResult.reason : undefined;
749
- logger.error("Block production failed", { ...logCtx, engineReason, bidReason });
750
- throw Error(`Block production failed: engine=${engineReason ?? "n/a"} builder=${bidReason ?? "n/a"}`);
765
+ const engineReason = engineResult.status === "rejected" ? engineResult.reason : engineResult.status;
766
+ const bidReason = bidResult.status === "rejected" ? bidResult.reason : bidResult.status;
767
+ logger.error("Block production failed", {
768
+ ...logCtx,
769
+ engineReason: String(engineReason),
770
+ bidReason: String(bidReason),
771
+ });
772
+ throw Error(`Block production failed: engine=${String(engineReason)} builder=${String(bidReason)}`);
751
773
  }
752
774
  const { block, executionPayloadValue, consensusBlockValue } = bestResult.value;
753
775
  metrics?.blockProductionSuccess.inc({ source });
@@ -764,13 +786,40 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
764
786
  if (chain.opts.persistProducedBlocks) {
765
787
  void chain.persistBlock(block, "produced_engine_block");
766
788
  }
789
+ // Include the payload for self-builds unless disabled (stateless flow)
790
+ const isSelfBuild = source === ProducedBlockSource.engine;
791
+ if (isSelfBuild && includePayload) {
792
+ const produceResult = chain.blockProductionCache.get(blockRoot);
793
+ if (produceResult === undefined ||
794
+ !isForkPostGloas(produceResult.fork) ||
795
+ produceResult.type !== BlockType.Full) {
796
+ throw Error(`Missing cached block production result for produced block slot=${slot} blockRoot=${blockRoot}`);
797
+ }
798
+ const { executionPayload, executionRequests, blobsBundle, parentBlockRoot } = produceResult;
799
+ const blockContents = {
800
+ block: block,
801
+ executionPayloadEnvelope: {
802
+ payload: executionPayload,
803
+ executionRequests,
804
+ builderIndex: BUILDER_INDEX_SELF_BUILD,
805
+ beaconBlockRoot: fromHex(blockRoot),
806
+ parentBeaconBlockRoot: parentBlockRoot,
807
+ },
808
+ kzgProofs: blobsBundle.proofs,
809
+ blobs: blobsBundle.blobs,
810
+ };
811
+ return {
812
+ data: blockContents,
813
+ meta: { version: fork, consensusBlockValue, executionPayloadValue, executionPayloadIncluded: true },
814
+ };
815
+ }
767
816
  return {
768
817
  data: block,
769
- meta: { version: fork, consensusBlockValue },
818
+ meta: { version: fork, consensusBlockValue, executionPayloadValue, executionPayloadIncluded: false },
770
819
  };
771
820
  },
772
821
  async produceAttestationData({ committeeIndex, slot }) {
773
- notWhileSyncing();
822
+ notWhileSyncing(chain, sync.state);
774
823
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
775
824
  // This needs a state in the same epoch as `slot` such that state.currentJustifiedCheckpoint is correct.
776
825
  // Note: This may trigger an epoch transition if there skipped slots at the beginning of the epoch.
@@ -841,12 +890,12 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
841
890
  if (!isForkPostGloas(fork)) {
842
891
  throw new ApiError(400, `producePayloadAttestationData is not supported before Gloas fork=${fork}`);
843
892
  }
844
- notWhileSyncing();
893
+ notWhileSyncing(chain, sync.state);
845
894
  await waitForSlot(slot);
846
895
  const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
847
896
  if (!block) {
848
- // No block is seen at slot. Return 404 so vc can skip casting payload attestation.
849
- throw new ApiError(404, `No canonical block found at slot=${slot}`);
897
+ // No canonical block is seen at slot. Return 204 so vc can skip casting payload attestation.
898
+ return { data: undefined, meta: { version: fork }, status: 204 };
850
899
  }
851
900
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
852
901
  // Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
@@ -910,7 +959,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
910
959
  return { data: contribution };
911
960
  },
912
961
  async getProposerDuties({ epoch }, _context, opts) {
913
- notWhileSyncing();
962
+ notWhileSyncing(chain, sync.state);
914
963
  const currentEpoch = currentEpochWithDisparity();
915
964
  const nextEpoch = currentEpoch + 1;
916
965
  const startSlot = computeStartSlotAtEpoch(epoch);
@@ -952,7 +1001,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
952
1001
  state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties);
953
1002
  }
954
1003
  else {
955
- const res = await getStateResponseWithRegen(chain, startSlot);
1004
+ const res = await getStateResponseWithRegen(chain, sync, startSlot);
956
1005
  state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state;
957
1006
  if (state.epoch !== epoch) {
958
1007
  throw Error(`Loaded state epoch ${state.epoch} does not match requested epoch ${epoch}`);
@@ -1028,7 +1077,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1028
1077
  return this.getProposerDuties(args, context, { v2: true });
1029
1078
  },
1030
1079
  async getAttesterDuties({ epoch, indices }) {
1031
- notWhileSyncing();
1080
+ notWhileSyncing(chain, sync.state);
1032
1081
  if (indices.length === 0) {
1033
1082
  throw new ApiError(400, "No validator to get attester duties");
1034
1083
  }
@@ -1076,7 +1125,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1076
1125
  };
1077
1126
  },
1078
1127
  async getPtcDuties({ epoch, indices }) {
1079
- notWhileSyncing();
1128
+ notWhileSyncing(chain, sync.state);
1080
1129
  if (indices.length === 0) {
1081
1130
  throw new ApiError(400, "No validator to get PTC duties");
1082
1131
  }
@@ -1129,7 +1178,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1129
1178
  * @param validatorIndices an array of the validator indices for which to obtain the duties.
1130
1179
  */
1131
1180
  async getSyncCommitteeDuties({ epoch, indices }) {
1132
- notWhileSyncing();
1181
+ notWhileSyncing(chain, sync.state);
1133
1182
  if (indices.length === 0) {
1134
1183
  throw new ApiError(400, "No validator to get attester duties");
1135
1184
  }
@@ -1169,7 +1218,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1169
1218
  };
1170
1219
  },
1171
1220
  async getAggregatedAttestationV2({ attestationDataRoot, slot, committeeIndex }) {
1172
- notWhileSyncing();
1221
+ notWhileSyncing(chain, sync.state);
1173
1222
  await waitForSlot(slot); // Must never request for a future slot > currentSlot
1174
1223
  const dataRootHex = toRootHex(attestationDataRoot);
1175
1224
  const aggregate = chain.attestationPool.getAggregate(slot, dataRootHex, committeeIndex);
@@ -1183,7 +1232,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1183
1232
  };
1184
1233
  },
1185
1234
  async publishAggregateAndProofsV2({ signedAggregateAndProofs }) {
1186
- notWhileSyncing();
1235
+ notWhileSyncing(chain, sync.state);
1187
1236
  const seenTimestampSec = Date.now() / 1000;
1188
1237
  const failures = [];
1189
1238
  const fork = chain.config.getForkName(chain.clock.currentSlot);
@@ -1229,7 +1278,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1229
1278
  * https://github.com/ethereum/beacon-APIs/pull/137
1230
1279
  */
1231
1280
  async publishContributionAndProofs({ contributionAndProofs }) {
1232
- notWhileSyncing();
1281
+ notWhileSyncing(chain, sync.state);
1233
1282
  const failures = [];
1234
1283
  await Promise.all(contributionAndProofs.map(async (contributionAndProof, i) => {
1235
1284
  try {
@@ -1262,7 +1311,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1262
1311
  }
1263
1312
  },
1264
1313
  async prepareBeaconCommitteeSubnet({ subscriptions }) {
1265
- notWhileSyncing();
1314
+ notWhileSyncing(chain, sync.state);
1266
1315
  await network.prepareBeaconCommitteeSubnets(subscriptions.map(({ validatorIndex, slot, isAggregator, committeesAtSlot, committeeIndex }) => ({
1267
1316
  validatorIndex: validatorIndex,
1268
1317
  subnet: computeSubnetForCommitteesAtSlot(slot, committeesAtSlot, committeeIndex),
@@ -1289,7 +1338,7 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1289
1338
  * https://github.com/ethereum/beacon-APIs/pull/136
1290
1339
  */
1291
1340
  async prepareSyncCommitteeSubnets({ subscriptions }) {
1292
- notWhileSyncing();
1341
+ notWhileSyncing(chain, sync.state);
1293
1342
  // A `validatorIndex` can be in multiple subnets, so compute the CommitteeSubscription with double for loop
1294
1343
  const subs = [];
1295
1344
  for (const sub of subscriptions) {
@@ -1364,12 +1413,45 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1364
1413
  count: filteredRegistrations.length,
1365
1414
  });
1366
1415
  },
1416
+ async submitProposerPreferences({ signedProposerPreferences }) {
1417
+ const failures = [];
1418
+ await Promise.all(signedProposerPreferences.map(async (signed, i) => {
1419
+ try {
1420
+ await validateGossipProposerPreferences(chain, signed);
1421
+ chain.proposerPreferencesPool.add(signed);
1422
+ await network.publishProposerPreferences(signed);
1423
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1424
+ version: config.getForkName(signed.message.proposalSlot),
1425
+ data: signed,
1426
+ });
1427
+ }
1428
+ catch (e) {
1429
+ const logCtx = {
1430
+ slot: signed.message.proposalSlot,
1431
+ validatorIndex: signed.message.validatorIndex,
1432
+ dependentRoot: toRootHex(signed.message.dependentRoot),
1433
+ };
1434
+ if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
1435
+ logger.debug("Ignoring known signed proposer preferences", logCtx);
1436
+ return;
1437
+ }
1438
+ failures.push({ index: i, message: e.message });
1439
+ logger.verbose(`Error on submitProposerPreferences [${i}]`, logCtx, e);
1440
+ if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
1441
+ chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
1442
+ }
1443
+ }
1444
+ }));
1445
+ if (failures.length > 0) {
1446
+ throw new IndexedError("Error processing signed proposer preferences", failures);
1447
+ }
1448
+ },
1367
1449
  async getExecutionPayloadEnvelope({ slot, beaconBlockRoot }) {
1368
1450
  const fork = config.getForkName(slot);
1369
1451
  if (!isForkPostGloas(fork)) {
1370
1452
  throw new ApiError(400, `getExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
1371
1453
  }
1372
- notWhileSyncing();
1454
+ notWhileSyncing(chain, sync.state);
1373
1455
  await waitForSlot(slot);
1374
1456
  const blockRootHex = toRootHex(beaconBlockRoot);
1375
1457
  const produceResult = chain.blockProductionCache.get(blockRootHex);
@@ -1383,6 +1465,13 @@ export function getValidatorApi(opts, { chain, config, logger, metrics, network,
1383
1465
  throw Error("Cached block production result is not full block");
1384
1466
  }
1385
1467
  const { executionPayload, executionRequests, parentBlockRoot } = produceResult;
1468
+ if (executionPayload === undefined) {
1469
+ // Blocks committing to a builder bid are cached as full but without payload data
1470
+ throw new ApiError(404, `No local execution payload cached for block root ${blockRootHex}`);
1471
+ }
1472
+ if (executionPayload.slotNumber !== slot) {
1473
+ throw new ApiError(404, `Cached execution payload is for slot=${executionPayload.slotNumber}, requested slot=${slot}`);
1474
+ }
1386
1475
  const envelope = {
1387
1476
  payload: executionPayload,
1388
1477
  executionRequests: executionRequests,