@lodestar/beacon-node 1.45.0-rc.0 → 1.46.0-dev.1cd9a671ea

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 (151) 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/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +172 -88
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/api/rest/base.d.ts.map +1 -1
  8. package/lib/api/rest/base.js +6 -3
  9. package/lib/api/rest/base.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  11. package/lib/chain/blocks/importBlock.js +2 -17
  12. package/lib/chain/blocks/importBlock.js.map +1 -1
  13. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  14. package/lib/chain/blocks/utils/chainSegment.js +13 -5
  15. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  16. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +1 -1
  17. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  18. package/lib/chain/blocks/verifyBlocksSanityChecks.js +1 -1
  19. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  21. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  22. package/lib/chain/chain.d.ts.map +1 -1
  23. package/lib/chain/chain.js +44 -2
  24. package/lib/chain/chain.js.map +1 -1
  25. package/lib/chain/errors/blockError.d.ts +48 -6
  26. package/lib/chain/errors/blockError.d.ts.map +1 -1
  27. package/lib/chain/errors/blockError.js +25 -4
  28. package/lib/chain/errors/blockError.js.map +1 -1
  29. package/lib/chain/errors/executionPayloadEnvelope.d.ts +11 -0
  30. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  31. package/lib/chain/errors/executionPayloadEnvelope.js +2 -0
  32. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  33. package/lib/chain/errors/proposerPreferences.d.ts +14 -1
  34. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  35. package/lib/chain/errors/proposerPreferences.js +2 -0
  36. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  37. package/lib/chain/lightClient/index.d.ts.map +1 -1
  38. package/lib/chain/lightClient/index.js +11 -6
  39. package/lib/chain/lightClient/index.js.map +1 -1
  40. package/lib/chain/lightClient/proofs.d.ts.map +1 -1
  41. package/lib/chain/lightClient/proofs.js +28 -2
  42. package/lib/chain/lightClient/proofs.js.map +1 -1
  43. package/lib/chain/lightClient/types.d.ts +3 -1
  44. package/lib/chain/lightClient/types.d.ts.map +1 -1
  45. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  46. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  47. package/lib/chain/opPools/proposerPreferencesPool.d.ts +1 -1
  48. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  49. package/lib/chain/opPools/proposerPreferencesPool.js +1 -1
  50. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  51. package/lib/chain/validation/aggregateAndProof.js +10 -6
  52. package/lib/chain/validation/aggregateAndProof.js.map +1 -1
  53. package/lib/chain/validation/attestation.d.ts.map +1 -1
  54. package/lib/chain/validation/attestation.js +10 -6
  55. package/lib/chain/validation/attestation.js.map +1 -1
  56. package/lib/chain/validation/block.d.ts.map +1 -1
  57. package/lib/chain/validation/block.js +53 -4
  58. package/lib/chain/validation/block.js.map +1 -1
  59. package/lib/chain/validation/executionPayloadBid.d.ts +5 -0
  60. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  61. package/lib/chain/validation/executionPayloadBid.js +31 -3
  62. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  63. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  64. package/lib/chain/validation/executionPayloadEnvelope.js +29 -0
  65. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  66. package/lib/chain/validation/proposerPreferences.d.ts +9 -2
  67. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  68. package/lib/chain/validation/proposerPreferences.js +61 -9
  69. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  70. package/lib/chain/validation/signatureSets/aggregateAndProof.d.ts.map +1 -1
  71. package/lib/chain/validation/signatureSets/aggregateAndProof.js +2 -3
  72. package/lib/chain/validation/signatureSets/aggregateAndProof.js.map +1 -1
  73. package/lib/db/repositories/lightclientSyncCommitteeWitness.d.ts.map +1 -1
  74. package/lib/db/repositories/lightclientSyncCommitteeWitness.js +42 -7
  75. package/lib/db/repositories/lightclientSyncCommitteeWitness.js.map +1 -1
  76. package/lib/network/gossip/encoding.d.ts +2 -2
  77. package/lib/network/gossip/encoding.d.ts.map +1 -1
  78. package/lib/network/gossip/encoding.js +15 -9
  79. package/lib/network/gossip/encoding.js.map +1 -1
  80. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  81. package/lib/network/gossip/gossipsub.js +2 -2
  82. package/lib/network/gossip/gossipsub.js.map +1 -1
  83. package/lib/network/gossip/topic.d.ts +74 -231
  84. package/lib/network/gossip/topic.d.ts.map +1 -1
  85. package/lib/network/gossip/topic.js +24 -1
  86. package/lib/network/gossip/topic.js.map +1 -1
  87. package/lib/network/interface.d.ts +4 -1
  88. package/lib/network/interface.d.ts.map +1 -1
  89. package/lib/network/network.d.ts +4 -1
  90. package/lib/network/network.d.ts.map +1 -1
  91. package/lib/network/network.js +19 -11
  92. package/lib/network/network.js.map +1 -1
  93. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  94. package/lib/network/processor/gossipHandlers.js +12 -6
  95. package/lib/network/processor/gossipHandlers.js.map +1 -1
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  97. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +4 -2
  98. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  99. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  100. package/lib/network/reqresp/handlers/index.js +20 -11
  101. package/lib/network/reqresp/handlers/index.js.map +1 -1
  102. package/lib/network/util.d.ts +1 -0
  103. package/lib/network/util.d.ts.map +1 -1
  104. package/lib/network/util.js +4 -0
  105. package/lib/network/util.js.map +1 -1
  106. package/lib/sync/sync.d.ts.map +1 -1
  107. package/lib/sync/sync.js +7 -0
  108. package/lib/sync/sync.js.map +1 -1
  109. package/lib/sync/unknownBlock.d.ts.map +1 -1
  110. package/lib/sync/unknownBlock.js +7 -2
  111. package/lib/sync/unknownBlock.js.map +1 -1
  112. package/lib/util/execution.js +1 -1
  113. package/lib/util/execution.js.map +1 -1
  114. package/package.json +18 -16
  115. package/src/api/impl/beacon/blocks/index.ts +221 -63
  116. package/src/api/impl/validator/index.ts +206 -98
  117. package/src/api/rest/base.ts +8 -3
  118. package/src/chain/blocks/importBlock.ts +0 -17
  119. package/src/chain/blocks/utils/chainSegment.ts +16 -5
  120. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +1 -1
  121. package/src/chain/blocks/verifyBlocksSanityChecks.ts +1 -1
  122. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +2 -2
  123. package/src/chain/chain.ts +61 -2
  124. package/src/chain/errors/blockError.ts +44 -7
  125. package/src/chain/errors/executionPayloadEnvelope.ts +9 -0
  126. package/src/chain/errors/proposerPreferences.ts +16 -1
  127. package/src/chain/lightClient/index.ts +12 -7
  128. package/src/chain/lightClient/proofs.ts +36 -1
  129. package/src/chain/lightClient/types.ts +3 -1
  130. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  131. package/src/chain/opPools/proposerPreferencesPool.ts +1 -1
  132. package/src/chain/validation/aggregateAndProof.ts +12 -6
  133. package/src/chain/validation/attestation.ts +12 -6
  134. package/src/chain/validation/block.ts +69 -4
  135. package/src/chain/validation/executionPayloadBid.ts +33 -3
  136. package/src/chain/validation/executionPayloadEnvelope.ts +37 -0
  137. package/src/chain/validation/proposerPreferences.ts +71 -9
  138. package/src/chain/validation/signatureSets/aggregateAndProof.ts +3 -3
  139. package/src/db/repositories/lightclientSyncCommitteeWitness.ts +57 -9
  140. package/src/network/gossip/encoding.ts +13 -7
  141. package/src/network/gossip/gossipsub.ts +7 -2
  142. package/src/network/gossip/topic.ts +30 -0
  143. package/src/network/interface.ts +3 -1
  144. package/src/network/network.ts +24 -15
  145. package/src/network/processor/gossipHandlers.ts +12 -6
  146. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +4 -2
  147. package/src/network/reqresp/handlers/index.ts +21 -12
  148. package/src/network/util.ts +5 -0
  149. package/src/sync/sync.ts +10 -0
  150. package/src/sync/unknownBlock.ts +8 -2
  151. 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
  }