@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
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  computeEpochAtSlot,
19
19
  computeTimeAtSlot,
20
+ isStatePostGloas,
20
21
  reconstructSignedBlockContents,
21
22
  signedBeaconBlockToBlinded,
22
23
  signedBlockToSignedHeader,
@@ -31,6 +32,7 @@ import {
31
32
  fulu,
32
33
  gloas,
33
34
  isDenebBlockContents,
35
+ isSignedExecutionPayloadEnvelopeContents,
34
36
  sszTypesFor,
35
37
  } from "@lodestar/types";
36
38
  import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils";
@@ -38,9 +40,16 @@ import {BlockInputSource, isBlockInputBlobs, isBlockInputColumns} from "../../..
38
40
  import {PayloadEnvelopeInputSource} from "../../../../chain/blocks/payloadEnvelopeInput/index.js";
39
41
  import {ImportBlockOpts} from "../../../../chain/blocks/types.js";
40
42
  import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js";
43
+ import {verifyExecutionPayloadEnvelope} from "../../../../chain/blocks/verifyExecutionPayloadEnvelope.js";
41
44
  import {BeaconChain} from "../../../../chain/chain.js";
42
45
  import {ChainEvent} from "../../../../chain/emitter.js";
43
- import {BlockError, BlockErrorCode, BlockGossipError} from "../../../../chain/errors/index.js";
46
+ import {
47
+ BlockError,
48
+ BlockErrorCode,
49
+ BlockGossipError,
50
+ ExecutionPayloadEnvelopeError,
51
+ ExecutionPayloadEnvelopeErrorCode,
52
+ } from "../../../../chain/errors/index.js";
44
53
  import {
45
54
  BlockType,
46
55
  ProduceFullBellatrix,
@@ -48,6 +57,7 @@ import {
48
57
  ProduceFullFulu,
49
58
  ProduceFullGloas,
50
59
  } from "../../../../chain/produceBlock/index.js";
60
+ import {RegenCaller} from "../../../../chain/regen/index.js";
51
61
  import {validateGossipBlock} from "../../../../chain/validation/block.js";
52
62
  import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js";
53
63
  import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js";
@@ -249,7 +259,7 @@ export function getBeaconBlockApi({
249
259
  "api_reject_parent_unknown"
250
260
  );
251
261
  throw new BlockError(signedBlock, {
252
- code: BlockErrorCode.PARENT_UNKNOWN,
262
+ code: BlockErrorCode.PARENT_BLOCK_UNKNOWN,
253
263
  parentRoot: toRootHex(signedBlock.message.parentRoot),
254
264
  });
255
265
  }
@@ -334,7 +344,8 @@ export function getBeaconBlockApi({
334
344
  .catch((e) => {
335
345
  if (
336
346
  e instanceof BlockError &&
337
- (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN)
347
+ (e.type.code === BlockErrorCode.PARENT_BLOCK_UNKNOWN ||
348
+ e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN)
338
349
  ) {
339
350
  chain.emitter.emit(ChainEvent.blockUnknownParent, {
340
351
  blockInput: blockForImport,
@@ -345,7 +356,9 @@ export function getBeaconBlockApi({
345
356
  throw e;
346
357
  }),
347
358
  ];
348
- const sentPeersArr = await promiseAllMaybeAsync<number | void>(publishPromises);
359
+ const sentPeersArr = await promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
360
+ publishPromises
361
+ );
349
362
 
350
363
  if (isForkPostGloas(fork)) {
351
364
  // After gloas, data columns are not published with the block but when publishing the execution payload envelope
@@ -356,10 +369,13 @@ export function getBeaconBlockApi({
356
369
  // + 1 because we publish to beacon_block first
357
370
  for (let i = 0; i < dataColumnSidecars.length; i++) {
358
371
  // + 1 because we publish to beacon_block first
359
- const sentPeers = sentPeersArr[i + 1] as number;
360
- // sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar() api
372
+ const {sentPeers, alreadyPublished} = sentPeersArr[i + 1] as {sentPeers: number; alreadyPublished: boolean};
373
+ // sent peers could be 0 as we set `allowPublishToZeroTopicPeers=true` in network.publishDataColumnSidecar()
361
374
  metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
362
- if (sentPeers === 0) {
375
+ // A duplicate publish (alreadyPublished=true) means the column is already propagating on the network —
376
+ // expected in self-build flows where peers gossip columns back to us before we publish the envelope.
377
+ // Only warn when we genuinely failed to reach any peer. See https://github.com/ChainSafe/lodestar/issues/9527.
378
+ if (sentPeers === 0 && !alreadyPublished) {
363
379
  columnsPublishedWithZeroPeers++;
364
380
  }
365
381
  }
@@ -651,8 +667,22 @@ export function getBeaconBlockApi({
651
667
  publishBlockV2,
652
668
  publishBlindedBlockV2,
653
669
 
654
- async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) {
670
+ async publishExecutionPayloadEnvelope({signedEnvelopeOrContents, broadcastValidation}) {
655
671
  const seenTimestampSec = Date.now() / 1000;
672
+
673
+ const blobDataIncluded = isSignedExecutionPayloadEnvelopeContents(signedEnvelopeOrContents);
674
+ let signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope;
675
+ // Blobs and KZG proofs submitted alongside the envelope in the stateless flow
676
+ let submittedContents: {kzgProofs: deneb.KZGProofs; blobs: deneb.Blobs} | null = null;
677
+
678
+ if (blobDataIncluded) {
679
+ signedExecutionPayloadEnvelope = signedEnvelopeOrContents.signedExecutionPayloadEnvelope;
680
+ submittedContents = {kzgProofs: signedEnvelopeOrContents.kzgProofs, blobs: signedEnvelopeOrContents.blobs};
681
+ } else {
682
+ // Stateful flow, blobs and KZG proofs are attached from the block production cache below
683
+ signedExecutionPayloadEnvelope = signedEnvelopeOrContents;
684
+ }
685
+
656
686
  const envelope = signedExecutionPayloadEnvelope.message;
657
687
  const slot = envelope.payload.slotNumber;
658
688
  const fork = config.getForkName(slot);
@@ -663,7 +693,6 @@ export function getBeaconBlockApi({
663
693
  throw new ApiError(400, `publishExecutionPayloadEnvelope not supported for pre-gloas fork=${fork}`);
664
694
  }
665
695
 
666
- // TODO GLOAS: review checks, do we want to implement `broadcast_validation`?
667
696
  let block = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.EMPTY);
668
697
  if (block === null) {
669
698
  // Only wait if the envelope is for the current slot
@@ -683,39 +712,172 @@ export function getBeaconBlockApi({
683
712
  throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`);
684
713
  }
685
714
 
686
- await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
687
-
688
715
  const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD;
689
- let dataColumnSidecars: gloas.DataColumnSidecar[] = [];
716
+ const cachedResult = chain.blockProductionCache.get(blockRootHex);
717
+ // Only use the cached result if it contains payload data, blocks committing to a bid are cached without it
718
+ const cachedGloasResult =
719
+ cachedResult !== undefined &&
720
+ isForkPostGloas(cachedResult.fork) &&
721
+ cachedResult.type === BlockType.Full &&
722
+ (cachedResult as ProduceFullGloas).executionPayload !== undefined
723
+ ? (cachedResult as ProduceFullGloas)
724
+ : undefined;
725
+
726
+ broadcastValidation = broadcastValidation ?? routes.beacon.BroadcastValidation.gossip;
727
+ const valLogMeta = {
728
+ slot,
729
+ blockRoot: blockRootHex,
730
+ blockHash: blockHashHex,
731
+ builderIndex: envelope.builderIndex,
732
+ isSelfBuild,
733
+ blobDataIncluded,
734
+ broadcastValidation,
735
+ ...(submittedContents !== null ? {submittedBlobs: submittedContents.blobs.length} : {}),
736
+ };
737
+ try {
738
+ switch (broadcastValidation) {
739
+ case routes.beacon.BroadcastValidation.none: {
740
+ chain.logger.debug("Skipping broadcast validation of execution payload envelope", valLogMeta);
741
+ break;
742
+ }
690
743
 
691
- if (isSelfBuild) {
692
- // For self-builds, construct and publish data column sidecars from cached block production data
693
- const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined;
694
- if (cachedResult === undefined) {
695
- throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`);
696
- }
697
- if (!isForkPostGloas(cachedResult.fork)) {
698
- throw new ApiError(400, `Cached block production result is for pre-gloas fork=${cachedResult.fork}`);
744
+ case routes.beacon.BroadcastValidation.gossip: {
745
+ await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
746
+ break;
747
+ }
748
+
749
+ case routes.beacon.BroadcastValidation.consensusAndEquivocation:
750
+ case routes.beacon.BroadcastValidation.consensus: {
751
+ await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
752
+
753
+ // Verify the envelope against the post-block state
754
+ const blockState = await chain.regen
755
+ .getBlockSlotState(block, block.slot, {dontTransferCache: true}, RegenCaller.restApi)
756
+ .catch((e) => {
757
+ chain.logger.debug("Failed to regenerate block state for consensus checks", valLogMeta, e as Error);
758
+ return null;
759
+ });
760
+ if (blockState === null || !isStatePostGloas(blockState)) {
761
+ throw new ApiError(
762
+ 500,
763
+ `Unable to regenerate block state for consensus checks slot=${slot} blockRoot=${blockRootHex}`
764
+ );
765
+ }
766
+ try {
767
+ // Signature and executionRequestsRoot are already verified by gossip validation above
768
+ verifyExecutionPayloadEnvelope(chain.config, blockState, envelope, {verifyExecutionRequestsRoot: false});
769
+ } catch (error) {
770
+ chain.logger.error(
771
+ "Consensus checks failed while publishing execution payload envelope",
772
+ valLogMeta,
773
+ error as Error
774
+ );
775
+ throw new ApiError(400, (error as Error).message);
776
+ }
777
+ chain.logger.debug("Consensus validated while publishing execution payload envelope", valLogMeta);
778
+
779
+ // TODO GLOAS: check the block is not a proposer equivocation before publishing the envelope
780
+ if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {
781
+ const message = `Equivocation checks not yet implemented for broadcastValidation=${broadcastValidation}`;
782
+ if (chain.opts.broadcastValidationStrictness === "error") {
783
+ throw Error(message);
784
+ }
785
+ chain.logger.warn(message, valLogMeta);
786
+ }
787
+ break;
788
+ }
789
+
790
+ default: {
791
+ const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
792
+ if (chain.opts.broadcastValidationStrictness === "error") {
793
+ throw Error(message);
794
+ }
795
+ chain.logger.warn(message, valLogMeta);
796
+ }
699
797
  }
700
- if (cachedResult.type !== BlockType.Full) {
701
- throw new ApiError(400, "Cached block production result is not full block");
798
+ } catch (error) {
799
+ if (
800
+ error instanceof ExecutionPayloadEnvelopeError &&
801
+ error.type.code === ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN
802
+ ) {
803
+ // The envelope may already be known, e.g. received via gossip from another node in a
804
+ // multi node setup, this is benign and treated as a successful publish (same as blocks)
805
+ if (submittedContents === null) {
806
+ chain.logger.debug("Ignoring already-known execution payload envelope during publishing", valLogMeta);
807
+ return;
808
+ }
809
+ // The envelope may have been gossiped without its data columns being published, e.g. if
810
+ // another beacon node failed mid-publish, still publish columns from the submitted blobs
811
+ chain.logger.debug("Publishing data columns of already-known execution payload envelope", valLogMeta);
812
+ } else {
813
+ throw error;
702
814
  }
815
+ }
703
816
 
704
- if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) {
705
- const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
706
- const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({
707
- cells: rowCells,
708
- proofs: cachedResult.blobsBundle.proofs.slice(
709
- rowIndex * NUMBER_OF_COLUMNS,
710
- (rowIndex + 1) * NUMBER_OF_COLUMNS
711
- ),
712
- }));
713
-
714
- dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs);
715
- timer?.();
817
+ const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
818
+ if (!payloadInput) {
819
+ // The block is awaited above (queuing if the envelope arrived first), and both the API and
820
+ // gossip import paths seed the PayloadEnvelopeInput before importing the block, so the input
821
+ // should exist here.
822
+ throw new ApiError(404, `PayloadEnvelopeInput not found for slot=${slot} blockRoot=${blockRootHex}`);
823
+ }
824
+
825
+ let dataColumnSidecars: gloas.DataColumnSidecar[] = [];
826
+ let cells: fulu.Cell[][] | undefined;
827
+ let kzgProofs: deneb.KZGProofs | undefined;
828
+ let dataColumnTimer: (() => number) | undefined;
829
+
830
+ if (submittedContents !== null) {
831
+ // Validate submitted blob data against bid commitments before computing data column sidecars
832
+ const expectedBlobCount = payloadInput.getVersionedHashes().length;
833
+ if (submittedContents.blobs.length !== expectedBlobCount) {
834
+ throw new ApiError(
835
+ 400,
836
+ `Submitted blob count does not match bid commitments submitted=${submittedContents.blobs.length} expected=${expectedBlobCount}`
837
+ );
838
+ }
839
+ const expectedProofCount = expectedBlobCount * NUMBER_OF_COLUMNS;
840
+ if (submittedContents.kzgProofs.length !== expectedProofCount) {
841
+ throw new ApiError(
842
+ 400,
843
+ `Submitted KZG proof count does not match bid commitments submitted=${submittedContents.kzgProofs.length} expected=${expectedProofCount}`
844
+ );
845
+ }
846
+ if (submittedContents.blobs.length > 0) {
847
+ // If the block was produced by this node, reuse the cached cells and only time the
848
+ // metric when cells are actually computed from the submitted blobs
849
+ if (cachedGloasResult?.cells) {
850
+ cells = cachedGloasResult.cells;
851
+ } else {
852
+ dataColumnTimer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
853
+ cells = submittedContents.blobs.map((blob) => kzg.computeCells(blob));
854
+ }
855
+ kzgProofs = submittedContents.kzgProofs;
856
+ }
857
+ } else if (cachedGloasResult !== undefined) {
858
+ if (cachedGloasResult.cells && cachedGloasResult.blobsBundle.commitments.length > 0) {
859
+ cells = cachedGloasResult.cells;
860
+ kzgProofs = cachedGloasResult.blobsBundle.proofs;
716
861
  }
717
862
  } else {
718
- // TODO GLOAS: will this api be used by builders or only for self-building?
863
+ // An envelope without blob data can only be published via the beacon node that cached them at block production
864
+ if (payloadInput.getVersionedHashes().length > 0) {
865
+ throw new ApiError(
866
+ 400,
867
+ `No cached blob data to attach to execution payload envelope for block root ${blockRootHex}`
868
+ );
869
+ }
870
+ }
871
+
872
+ if (cells !== undefined && kzgProofs !== undefined && cells.length > 0) {
873
+ const proofs = kzgProofs;
874
+ const cellsAndProofs = cells.map((rowCells, rowIndex) => ({
875
+ cells: rowCells,
876
+ proofs: proofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS),
877
+ }));
878
+
879
+ dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs);
880
+ dataColumnTimer?.();
719
881
  }
720
882
 
721
883
  // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
@@ -724,21 +886,18 @@ export function getBeaconBlockApi({
724
886
  await sleep(msToBlockSlot);
725
887
  }
726
888
 
727
- const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
728
- if (!payloadInput) {
729
- // The block is awaited above (queuing if the envelope arrived first), and both the API and
730
- // gossip import paths seed the PayloadEnvelopeInput before importing the block, so the input
731
- // should exist here.
732
- throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`);
889
+ if (payloadInput.hasPayloadEnvelope()) {
890
+ // The envelope may have been added while this request was being validated, e.g. via gossip
891
+ chain.logger.debug("Execution payload envelope already added during publishing", valLogMeta);
892
+ } else {
893
+ payloadInput.addPayloadEnvelope({
894
+ envelope: signedExecutionPayloadEnvelope,
895
+ source: PayloadEnvelopeInputSource.api,
896
+ seenTimestampSec,
897
+ peerIdStr: undefined,
898
+ });
733
899
  }
734
900
 
735
- payloadInput.addPayloadEnvelope({
736
- envelope: signedExecutionPayloadEnvelope,
737
- source: PayloadEnvelopeInputSource.api,
738
- seenTimestampSec,
739
- peerIdStr: undefined,
740
- });
741
-
742
901
  if (dataColumnSidecars.length > 0) {
743
902
  for (const columnSidecar of dataColumnSidecars) {
744
903
  payloadInput.addColumn({
@@ -750,31 +909,28 @@ export function getBeaconBlockApi({
750
909
  }
751
910
  }
752
911
 
753
- const valLogMeta = {
754
- slot,
755
- blockRoot: blockRootHex,
756
- blockHash: blockHashHex,
757
- builderIndex: envelope.builderIndex,
758
- isSelfBuild,
759
- dataColumns: dataColumnSidecars.length,
760
- };
761
-
762
912
  const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
763
913
  metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec);
764
914
  chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.api, delaySec, signedExecutionPayloadEnvelope);
765
915
 
766
- chain.logger.info("Publishing execution payload envelope", valLogMeta);
916
+ chain.logger.info("Publishing execution payload envelope", {
917
+ ...valLogMeta,
918
+ dataColumns: dataColumnSidecars.length,
919
+ });
767
920
 
768
921
  const publishPromises = [
769
922
  // Gossip the signed execution payload envelope first
770
923
  () => network.publishSignedExecutionPayloadEnvelope(signedExecutionPayloadEnvelope),
771
- // For self-builds, publish all data column sidecars
924
+ // Publish all data column sidecars
772
925
  ...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
773
- // Import execution payload. Signature already verified above
926
+ // Import execution payload. Signature is verified during broadcast validation, an already
927
+ // known envelope was verified by its original source and `none` deliberately skips validation
774
928
  () => chain.processExecutionPayload(payloadInput, {validSignature: true}),
775
929
  ];
776
930
 
777
- const publishPromise = promiseAllMaybeAsync<number | void>(publishPromises);
931
+ const publishPromise = promiseAllMaybeAsync<{sentPeers: number; alreadyPublished: boolean} | number | void>(
932
+ publishPromises
933
+ );
778
934
 
779
935
  chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
780
936
  slot,
@@ -790,9 +946,11 @@ export function getBeaconBlockApi({
790
946
  let columnsPublishedWithZeroPeers = 0;
791
947
  // Skip first entry (envelope); the final entry is processExecutionPayload(), which returns void.
792
948
  for (let i = 0; i < dataColumnSidecars.length; i++) {
793
- const sentPeers = sentPeersArr[i + 1] as number;
949
+ const {sentPeers, alreadyPublished} = sentPeersArr[i + 1] as {sentPeers: number; alreadyPublished: boolean};
794
950
  metrics?.dataColumns.sentPeersPerSubnet.observe(sentPeers);
795
- if (sentPeers === 0) {
951
+ // A duplicate publish means the column is already propagating — expected in self-build flows.
952
+ // Only warn when we genuinely failed to reach any peer. See https://github.com/ChainSafe/lodestar/issues/9527.
953
+ if (sentPeers === 0 && !alreadyPublished) {
796
954
  columnsPublishedWithZeroPeers++;
797
955
  }
798
956
  }
@@ -7,7 +7,7 @@ import {getBeaconRewardsApi} from "./rewards/index.js";
7
7
  import {getBeaconStateApi} from "./state/index.js";
8
8
 
9
9
  export function getBeaconApi(
10
- modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db">
10
+ modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db" | "sync">
11
11
  ): ApplicationMethods<routes.beacon.Endpoints> {
12
12
  const block = getBeaconBlockApi(modules);
13
13
  const pool = getBeaconPoolApi(modules);
@@ -241,6 +241,11 @@ export function getBeaconPoolApi({
241
241
  payloadAttestationMessage.data.blobDataAvailable
242
242
  );
243
243
 
244
+ chain.emitter.emit(routes.events.EventType.payloadAttestationMessage, {
245
+ version: chain.config.getForkName(slot),
246
+ data: payloadAttestationMessage,
247
+ });
248
+
244
249
  await network.publishPayloadAttestationMessage(payloadAttestationMessage);
245
250
  } catch (e) {
246
251
  const logCtx = {
@@ -26,11 +26,12 @@ import {
26
26
  export function getBeaconStateApi({
27
27
  chain,
28
28
  config,
29
- }: Pick<ApiModules, "chain" | "config">): ApplicationMethods<routes.beacon.state.Endpoints> {
29
+ sync,
30
+ }: Pick<ApiModules, "chain" | "config" | "sync">): ApplicationMethods<routes.beacon.state.Endpoints> {
30
31
  async function getState(
31
32
  stateId: routes.beacon.StateId
32
33
  ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean}> {
33
- const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId);
34
+ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, sync, stateId);
34
35
 
35
36
  return {
36
37
  state: state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state,
@@ -14,7 +14,9 @@ import {
14
14
  } from "@lodestar/types";
15
15
  import {byteArrayEquals, fromHex} from "@lodestar/utils";
16
16
  import {IBeaconChain} from "../../../../chain/index.js";
17
+ import {IBeaconSync} from "../../../../sync/index.js";
17
18
  import {ApiError, ValidationError} from "../../errors.js";
19
+ import {notWhileSyncing} from "../../utils.js";
18
20
 
19
21
  export function resolveStateId(
20
22
  forkChoice: IForkChoice,
@@ -51,8 +53,17 @@ export function resolveStateId(
51
53
 
52
54
  export async function getStateResponseWithRegen(
53
55
  chain: IBeaconChain,
56
+ sync: IBeaconSync,
54
57
  inStateId: routes.beacon.StateId
55
58
  ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean}> {
59
+ // "head", "finalized" and "justified" resolve to already-available cached states, and "genesis" to a
60
+ // historical DB read - none trigger the forward regen that can walk back past the block-root window
61
+ // (SLOTS_PER_HISTORICAL_ROOT) and wedge a far-behind node. Keep serving those (node observability,
62
+ // dashboards, validator client checks) even while syncing; guard only the regen-capable lookups.
63
+ if (inStateId !== "head" && inStateId !== "finalized" && inStateId !== "justified" && inStateId !== "genesis") {
64
+ notWhileSyncing(chain, sync.state);
65
+ }
66
+
56
67
  const stateId = resolveStateId(chain.forkChoice, inStateId);
57
68
 
58
69
  const res =
@@ -39,7 +39,8 @@ function toPayloadStatusName(status: PayloadStatus): "pending" | "empty" | "full
39
39
  export function getDebugApi({
40
40
  chain,
41
41
  config,
42
- }: Pick<ApiModules, "chain" | "config" | "db">): ApplicationMethods<routes.debug.Endpoints> {
42
+ sync,
43
+ }: Pick<ApiModules, "chain" | "config" | "db" | "sync">): ApplicationMethods<routes.debug.Endpoints> {
43
44
  return {
44
45
  async getDebugChainHeadsV2() {
45
46
  const heads = chain.forkChoice.getHeads();
@@ -132,7 +133,7 @@ export function getDebugApi({
132
133
  },
133
134
 
134
135
  async getStateV2({stateId}, context) {
135
- const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId);
136
+ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, sync, stateId);
136
137
  let slot: number, data: Uint8Array | BeaconState;
137
138
  if (state instanceof Uint8Array) {
138
139
  slot = getStateSlotFromBytes(state);
@@ -217,7 +217,7 @@ export function getLodestarApi({
217
217
  },
218
218
 
219
219
  async getHistoricalSummaries({stateId}) {
220
- const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId);
220
+ const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, sync, stateId);
221
221
 
222
222
  const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
223
223
 
@@ -332,7 +332,7 @@ export function getLodestarApi({
332
332
 
333
333
  for (const [epoch, attestationsPerEpoch] of attestations) {
334
334
  const slot = computeStartSlotAtEpoch(epoch);
335
- const {state} = await getStateResponseWithRegen(chain, slot);
335
+ const {state} = await getStateResponseWithRegen(chain, sync, slot);
336
336
  const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state;
337
337
  const shuffling = stateView.getShufflingAtEpoch(epoch);
338
338
  for (const attestation of attestationsPerEpoch) {
@@ -8,7 +8,7 @@ import {ApiModules} from "../types.js";
8
8
 
9
9
  export function getProofApi(
10
10
  opts: ApiOptions,
11
- {chain, config}: Pick<ApiModules, "chain" | "config" | "db">
11
+ {chain, config, sync}: Pick<ApiModules, "chain" | "config" | "db" | "sync">
12
12
  ): ApplicationMethods<routes.proof.Endpoints> {
13
13
  // It's currently possible to request gigantic proofs (eg: a proof of the entire beacon state)
14
14
  // We want some some sort of resistance against this DoS vector.
@@ -21,7 +21,7 @@ export function getProofApi(
21
21
  throw new Error("Requested proof is too large.");
22
22
  }
23
23
 
24
- const res = await getStateResponseWithRegen(chain, stateId);
24
+ const res = await getStateResponseWithRegen(chain, sync, stateId);
25
25
 
26
26
  const state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state;
27
27
 
@@ -1,4 +1,54 @@
1
- import {ApiError} from "./errors.js";
1
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import type {IBeaconChain} from "../../chain/index.js";
3
+ import {SyncState} from "../../sync/index.js";
4
+ import {ApiError, NodeIsSyncing} from "./errors.js";
5
+
6
+ /**
7
+ * If the node is within this many epochs from the head, we declare it to be synced regardless of
8
+ * the network sync state.
9
+ *
10
+ * This helps prevent attacks where nodes can convince us that we're syncing some non-existent
11
+ * finalized head.
12
+ *
13
+ * TODO: Lighthouse uses 8 for the attack described above. However, 8 kills Lodestar since validators
14
+ * can trigger regen to fast-forward head state 8 epochs to be immediately invalidated as sync sets
15
+ * a new head. Then the checkpoint state cache grows unbounded with very different states (because
16
+ * they are 8 epochs apart) and causes an OOM. Research a proper solution once regen and the state
17
+ * caches are better.
18
+ */
19
+ export const SYNC_TOLERANCE_EPOCHS = 1;
20
+
21
+ /**
22
+ * Reject any request while the node is syncing. Used by endpoints that must not serve while the
23
+ * node is behind — validator duties, and beacon state lookups whose regen could otherwise walk
24
+ * back past the block-root window (`SLOTS_PER_HISTORICAL_ROOT`) and wedge the node. Throws
25
+ * {@link NodeIsSyncing} (503).
26
+ */
27
+ export function notWhileSyncing(chain: IBeaconChain, syncState: SyncState): void {
28
+ // Consider node synced before or close to genesis
29
+ if (chain.clock.currentSlot < SLOTS_PER_EPOCH) {
30
+ return;
31
+ }
32
+
33
+ switch (syncState) {
34
+ case SyncState.SyncingFinalized:
35
+ case SyncState.SyncingHead: {
36
+ const currentSlot = chain.clock.currentSlot;
37
+ const headSlot = chain.forkChoice.getHead().slot;
38
+ if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) {
39
+ throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`);
40
+ }
41
+
42
+ return;
43
+ }
44
+
45
+ case SyncState.Synced:
46
+ return;
47
+
48
+ case SyncState.Stalled:
49
+ throw new NodeIsSyncing("waiting for peers");
50
+ }
51
+ }
2
52
 
3
53
  /**
4
54
  * Ensures that the array contains unique values, and throws an ApiError