@lodestar/beacon-node 1.48.0-dev.67344272c0 → 1.48.0-dev.68ce7b06a0

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 (170) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +4 -11
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/utils.js +14 -0
  6. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  7. package/lib/api/impl/config/index.d.ts.map +1 -1
  8. package/lib/api/impl/config/index.js +4 -1
  9. package/lib/api/impl/config/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts.map +1 -1
  11. package/lib/api/impl/validator/index.js +39 -39
  12. package/lib/api/impl/validator/index.js.map +1 -1
  13. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  14. package/lib/chain/archiveStore/archiveStore.js +4 -1
  15. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  16. package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
  17. package/lib/chain/archiveStore/historicalState/metrics.js +12 -1
  18. package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
  19. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +9 -1
  20. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  21. package/lib/chain/archiveStore/utils/archiveBlocks.js +37 -4
  22. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  23. package/lib/chain/blocks/importBlock.d.ts +5 -1
  24. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  25. package/lib/chain/blocks/importBlock.js +12 -18
  26. package/lib/chain/blocks/importBlock.js.map +1 -1
  27. package/lib/chain/blocks/index.d.ts +8 -4
  28. package/lib/chain/blocks/index.d.ts.map +1 -1
  29. package/lib/chain/blocks/index.js +83 -17
  30. package/lib/chain/blocks/index.js.map +1 -1
  31. package/lib/chain/blocks/types.d.ts +8 -0
  32. package/lib/chain/blocks/types.d.ts.map +1 -1
  33. package/lib/chain/blocks/types.js.map +1 -1
  34. package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
  35. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  36. package/lib/chain/blocks/utils/chainSegment.js +26 -11
  37. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  38. package/lib/chain/chain.d.ts +9 -4
  39. package/lib/chain/chain.d.ts.map +1 -1
  40. package/lib/chain/chain.js +82 -51
  41. package/lib/chain/chain.js.map +1 -1
  42. package/lib/chain/emitter.d.ts +1 -1
  43. package/lib/chain/emitter.js +1 -1
  44. package/lib/chain/errors/blockError.d.ts +3 -5
  45. package/lib/chain/errors/blockError.d.ts.map +1 -1
  46. package/lib/chain/errors/blockError.js +2 -2
  47. package/lib/chain/errors/blockError.js.map +1 -1
  48. package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
  49. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  50. package/lib/chain/errors/executionPayloadBid.js +1 -0
  51. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  52. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  53. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  54. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  55. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  56. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  57. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  58. package/lib/chain/errors/gossipValidation.js +1 -0
  59. package/lib/chain/errors/gossipValidation.js.map +1 -1
  60. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  61. package/lib/chain/forkChoice/index.js +2 -0
  62. package/lib/chain/forkChoice/index.js.map +1 -1
  63. package/lib/chain/interface.d.ts +4 -4
  64. package/lib/chain/interface.d.ts.map +1 -1
  65. package/lib/chain/lightClient/index.d.ts +3 -3
  66. package/lib/chain/lightClient/index.d.ts.map +1 -1
  67. package/lib/chain/lightClient/index.js +21 -23
  68. package/lib/chain/lightClient/index.js.map +1 -1
  69. package/lib/chain/lightClient/proofs.d.ts +2 -1
  70. package/lib/chain/lightClient/proofs.d.ts.map +1 -1
  71. package/lib/chain/lightClient/proofs.js +5 -1
  72. package/lib/chain/lightClient/proofs.js.map +1 -1
  73. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  74. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  75. package/lib/chain/prepareNextSlot.js +13 -13
  76. package/lib/chain/prepareNextSlot.js.map +1 -1
  77. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  78. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  79. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  80. package/lib/chain/validation/block.d.ts.map +1 -1
  81. package/lib/chain/validation/block.js +9 -50
  82. package/lib/chain/validation/block.js.map +1 -1
  83. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  84. package/lib/chain/validation/executionPayloadBid.js +56 -40
  85. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  86. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  87. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  88. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  89. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  90. package/lib/chain/validation/payloadAttestationMessage.js +5 -6
  91. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  92. package/lib/chain/validatorMonitor.d.ts +7 -1
  93. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  94. package/lib/chain/validatorMonitor.js +22 -5
  95. package/lib/chain/validatorMonitor.js.map +1 -1
  96. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  97. package/lib/execution/builder/validateBid.js +8 -5
  98. package/lib/execution/builder/validateBid.js.map +1 -1
  99. package/lib/execution/engine/mock.d.ts.map +1 -1
  100. package/lib/execution/engine/mock.js +4 -1
  101. package/lib/execution/engine/mock.js.map +1 -1
  102. package/lib/metrics/metrics/lodestar.d.ts +4 -3
  103. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  104. package/lib/metrics/metrics/lodestar.js +5 -5
  105. package/lib/metrics/metrics/lodestar.js.map +1 -1
  106. package/lib/network/gossip/encoding.d.ts.map +1 -1
  107. package/lib/network/gossip/encoding.js +0 -6
  108. package/lib/network/gossip/encoding.js.map +1 -1
  109. package/lib/network/gossip/topic.d.ts +2 -1
  110. package/lib/network/gossip/topic.d.ts.map +1 -1
  111. package/lib/network/gossip/topic.js +4 -20
  112. package/lib/network/gossip/topic.js.map +1 -1
  113. package/lib/network/network.js +2 -2
  114. package/lib/network/network.js.map +1 -1
  115. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  116. package/lib/network/processor/gossipHandlers.js +383 -279
  117. package/lib/network/processor/gossipHandlers.js.map +1 -1
  118. package/lib/network/processor/index.d.ts +49 -3
  119. package/lib/network/processor/index.d.ts.map +1 -1
  120. package/lib/network/processor/index.js +254 -114
  121. package/lib/network/processor/index.js.map +1 -1
  122. package/lib/network/reqresp/protocols.d.ts.map +1 -1
  123. package/lib/network/reqresp/protocols.js +6 -22
  124. package/lib/network/reqresp/protocols.js.map +1 -1
  125. package/lib/network/reqresp/rateLimit.js +2 -1
  126. package/lib/network/reqresp/rateLimit.js.map +1 -1
  127. package/lib/sync/range/range.d.ts.map +1 -1
  128. package/lib/sync/range/range.js +24 -1
  129. package/lib/sync/range/range.js.map +1 -1
  130. package/package.json +15 -15
  131. package/src/api/impl/beacon/blocks/index.ts +8 -14
  132. package/src/api/impl/beacon/state/utils.ts +15 -0
  133. package/src/api/impl/config/index.ts +4 -1
  134. package/src/api/impl/validator/index.ts +44 -43
  135. package/src/chain/archiveStore/archiveStore.ts +6 -0
  136. package/src/chain/archiveStore/historicalState/metrics.ts +14 -1
  137. package/src/chain/archiveStore/utils/archiveBlocks.ts +39 -3
  138. package/src/chain/blocks/importBlock.ts +25 -20
  139. package/src/chain/blocks/index.ts +120 -23
  140. package/src/chain/blocks/types.ts +9 -0
  141. package/src/chain/blocks/utils/chainSegment.ts +26 -12
  142. package/src/chain/chain.ts +100 -61
  143. package/src/chain/emitter.ts +1 -1
  144. package/src/chain/errors/blockError.ts +3 -3
  145. package/src/chain/errors/executionPayloadBid.ts +2 -0
  146. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  147. package/src/chain/errors/gossipValidation.ts +1 -0
  148. package/src/chain/forkChoice/index.ts +2 -0
  149. package/src/chain/interface.ts +4 -4
  150. package/src/chain/lightClient/index.ts +32 -36
  151. package/src/chain/lightClient/proofs.ts +10 -0
  152. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  153. package/src/chain/prepareNextSlot.ts +16 -16
  154. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  155. package/src/chain/validation/block.ts +9 -66
  156. package/src/chain/validation/executionPayloadBid.ts +62 -44
  157. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
  158. package/src/chain/validation/payloadAttestationMessage.ts +8 -9
  159. package/src/chain/validatorMonitor.ts +24 -7
  160. package/src/execution/builder/validateBid.ts +10 -6
  161. package/src/execution/engine/mock.ts +4 -1
  162. package/src/metrics/metrics/lodestar.ts +6 -5
  163. package/src/network/gossip/encoding.ts +0 -6
  164. package/src/network/gossip/topic.ts +3 -25
  165. package/src/network/network.ts +2 -2
  166. package/src/network/processor/gossipHandlers.ts +495 -345
  167. package/src/network/processor/index.ts +287 -165
  168. package/src/network/reqresp/protocols.ts +7 -24
  169. package/src/network/reqresp/rateLimit.ts +2 -1
  170. package/src/sync/range/range.ts +23 -1
@@ -87,6 +87,7 @@ import {OpSource} from "../../chain/validatorMonitor.js";
87
87
  import {Metrics} from "../../metrics/index.js";
88
88
  import {kzgCommitmentToVersionedHash} from "../../util/blobs.js";
89
89
  import {getBlobKzgCommitments, getDataColumnSidecarSlot} from "../../util/dataColumns.js";
90
+ import {callInNextEventLoop} from "../../util/eventLoop.js";
90
91
  import {INetworkCore} from "../core/index.js";
91
92
  import {NetworkEventBus} from "../events.js";
92
93
  import {
@@ -248,7 +249,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
248
249
  throw e;
249
250
  }
250
251
 
251
- chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`);
252
+ chain.persistInvalidSszValue(
253
+ forkTypes.SignedBeaconBlock,
254
+ signedBlock,
255
+ `gossip_reject_slot_${slot}`,
256
+ blockRootHex
257
+ );
252
258
  }
253
259
 
254
260
  // REJECT or unexpected (non-BlockGossipError) error: drop the optimistically-added entries from
@@ -717,8 +723,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
717
723
  const signedBlock = sszDeserialize(topic, serializedData);
718
724
  try {
719
725
  const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
720
- chain.serializedCache.set(signedBlock, serializedData);
721
- handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
726
+ // Handler - deferred to next event loop so the validation result propagates first
727
+ callInNextEventLoop(() => {
728
+ try {
729
+ chain.serializedCache.set(signedBlock, serializedData);
730
+ handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
731
+ } catch (e) {
732
+ logger.debug(
733
+ "Error handling gossip block",
734
+ {slot: signedBlock.message.slot, root: blockInput.blockRootHex},
735
+ e as Error
736
+ );
737
+ }
738
+ });
722
739
  } catch (e) {
723
740
  // Spec: IGNORE the block, ie not to re-publish to peers
724
741
  // but we should still import an equivocating (REPEAT_PROPOSAL) block into fork choice because we don't
@@ -732,6 +749,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
732
749
  // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
733
750
  const blockInput = chain.seenBlockInputCache.get(e.type.root);
734
751
  if (blockInput) {
752
+ // We're returning IGNORE (thrown below), so this block is not forwarded to peers. Unlike the
753
+ // happy path there is no rush to forward, so we don't need to wrap in callInNextEventLoop.
735
754
  chain.serializedCache.set(signedBlock, serializedData);
736
755
  // this is technically not a valid gossip block but gossip validation is a cheap subset of checks
737
756
  // this runs the full state transition, so importing an equivocating-but-valid block here is safe.
@@ -758,29 +777,40 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
758
777
  throw new GossipActionError(GossipAction.REJECT, {code: "PRE_DENEB_BLOCK"});
759
778
  }
760
779
  const blockInput = await validateBeaconBlob(blobSidecar, topic.subnet, peerIdStr, seenTimestampSec);
761
- chain.serializedCache.set(blobSidecar, serializedData);
762
- if (!blockInput.hasBlockAndAllData()) {
763
- const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
764
- chain.logger.debug("Received gossip blob, waiting for full data availability", {
765
- msToWait: cutoffTimeMs,
766
- blobIndex: index,
767
- ...blockInput.getLogMeta(),
768
- });
769
- blockInput.waitForAllData(cutoffTimeMs).catch((_e) => {
770
- chain.logger.debug(
771
- "Waited for data after receiving gossip blob. Cut-off reached so attempting to fetch remainder of BlockInput",
772
- {
780
+ // Handler - deferred to next event loop so the validation result propagates first
781
+ callInNextEventLoop(() => {
782
+ try {
783
+ chain.serializedCache.set(blobSidecar, serializedData);
784
+ if (!blockInput.hasBlockAndAllData()) {
785
+ const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
786
+ chain.logger.debug("Received gossip blob, waiting for full data availability", {
787
+ msToWait: cutoffTimeMs,
773
788
  blobIndex: index,
774
789
  ...blockInput.getLogMeta(),
775
- }
790
+ });
791
+ blockInput.waitForAllData(cutoffTimeMs).catch((_e) => {
792
+ chain.logger.debug(
793
+ "Waited for data after receiving gossip blob. Cut-off reached so attempting to fetch remainder of BlockInput",
794
+ {
795
+ blobIndex: index,
796
+ ...blockInput.getLogMeta(),
797
+ }
798
+ );
799
+ chain.emitter.emit(ChainEvent.incompleteBlockInput, {
800
+ blockInput,
801
+ peer: peerIdStr,
802
+ source: BlockInputSource.gossip,
803
+ });
804
+ });
805
+ }
806
+ } catch (e) {
807
+ logger.debug(
808
+ "Error handling gossip blob",
809
+ {slot: blobSlot, root: blockInput.blockRootHex, index},
810
+ e as Error
776
811
  );
777
- chain.emitter.emit(ChainEvent.incompleteBlockInput, {
778
- blockInput,
779
- peer: peerIdStr,
780
- source: BlockInputSource.gossip,
781
- });
782
- });
783
- }
812
+ }
813
+ });
784
814
  },
785
815
 
786
816
  [GossipType.data_column_sidecar]: async ({
@@ -813,58 +843,69 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
813
843
  peerIdStr,
814
844
  seenTimestampSec
815
845
  );
816
- chain.serializedCache.set(dataColumnSidecar, serializedData);
817
-
818
- const payloadInputMeta = payloadInput.getLogMeta();
819
- const {receivedColumns} = payloadInputMeta;
820
- // it's not helpful to track every single column received
821
- // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
822
- switch (receivedColumns) {
823
- case 1:
824
- case config.SAMPLES_PER_SLOT:
825
- case 2 * config.SAMPLES_PER_SLOT:
826
- case NUMBER_OF_COLUMNS / 4:
827
- case NUMBER_OF_COLUMNS / 2:
828
- case NUMBER_OF_COLUMNS:
829
- metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec);
830
- break;
831
- }
832
-
833
- if (!payloadInput.hasComputedAllData()) {
834
- // if we've received at least half of the columns, trigger reconstruction of the rest
835
- if (receivedColumns >= NUMBER_OF_COLUMNS / 2) {
836
- chain.columnReconstructionTracker.triggerColumnReconstruction(payloadInput);
837
- }
846
+ // Handler - deferred to next event loop so the validation result propagates first
847
+ callInNextEventLoop(() => {
848
+ try {
849
+ chain.serializedCache.set(dataColumnSidecar, serializedData);
850
+
851
+ const payloadInputMeta = payloadInput.getLogMeta();
852
+ const {receivedColumns} = payloadInputMeta;
853
+ // it's not helpful to track every single column received
854
+ // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
855
+ switch (receivedColumns) {
856
+ case 1:
857
+ case config.SAMPLES_PER_SLOT:
858
+ case 2 * config.SAMPLES_PER_SLOT:
859
+ case NUMBER_OF_COLUMNS / 4:
860
+ case NUMBER_OF_COLUMNS / 2:
861
+ case NUMBER_OF_COLUMNS:
862
+ metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec);
863
+ break;
864
+ }
838
865
 
839
- chain.logger.debug("Received gossip data column, payload envelope input not yet complete", {
840
- dataColumnIndex: index,
841
- ...payloadInputMeta,
842
- });
843
- }
866
+ if (!payloadInput.hasComputedAllData()) {
867
+ // if we've received at least half of the columns, trigger reconstruction of the rest
868
+ if (receivedColumns >= NUMBER_OF_COLUMNS / 2) {
869
+ chain.columnReconstructionTracker.triggerColumnReconstruction(payloadInput);
870
+ }
844
871
 
845
- // NOTE: we do NOT call chain.processExecutionPayload here. That is triggered only by
846
- // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
847
- // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
848
-
849
- if (!payloadInput.isComplete()) {
850
- const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
851
- // do not await here to not delay gossip validation
852
- payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
853
- chain.logger.debug(
854
- "Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope",
855
- {
872
+ chain.logger.debug("Received gossip data column, payload envelope input not yet complete", {
856
873
  dataColumnIndex: index,
857
874
  ...payloadInputMeta,
858
- }
875
+ });
876
+ }
877
+
878
+ // NOTE: we do NOT call chain.processExecutionPayload here. That is triggered only by
879
+ // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
880
+ // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
881
+
882
+ if (!payloadInput.isComplete()) {
883
+ const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
884
+ // do not await here to not delay gossip validation
885
+ payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
886
+ chain.logger.debug(
887
+ "Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope",
888
+ {
889
+ dataColumnIndex: index,
890
+ ...payloadInputMeta,
891
+ }
892
+ );
893
+ // TODO GLOAS: UnknownBlockSync to handle this event
894
+ chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
895
+ payloadInput,
896
+ peer: peerIdStr,
897
+ source: BlockInputSource.gossip,
898
+ });
899
+ });
900
+ }
901
+ } catch (e) {
902
+ logger.debug(
903
+ "Error handling gossip data column",
904
+ {slot: dataColumnSlot, root: payloadInput.blockRootHex, index},
905
+ e as Error
859
906
  );
860
- // TODO GLOAS: UnknownBlockSync to handle this event
861
- chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
862
- payloadInput,
863
- peer: peerIdStr,
864
- source: BlockInputSource.gossip,
865
- });
866
- });
867
- }
907
+ }
908
+ });
868
909
  } else {
869
910
  if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) {
870
911
  throw new GossipActionError(GossipAction.REJECT, {code: "PRE_FULU_BLOCK"});
@@ -887,54 +928,65 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
887
928
  peerIdStr,
888
929
  seenTimestampSec
889
930
  );
890
- chain.serializedCache.set(dataColumnSidecar, serializedData);
891
- const blockInputMeta = blockInput.getLogMeta();
892
- const {receivedColumns} = blockInputMeta;
893
- // it's not helpful to track every single column received
894
- // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
895
- switch (receivedColumns) {
896
- case 1:
897
- case config.SAMPLES_PER_SLOT:
898
- case 2 * config.SAMPLES_PER_SLOT:
899
- case NUMBER_OF_COLUMNS / 4:
900
- case NUMBER_OF_COLUMNS / 2:
901
- case NUMBER_OF_COLUMNS:
902
- metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec);
903
- break;
904
- }
931
+ // Handler - deferred to next event loop so the validation result propagates first
932
+ callInNextEventLoop(() => {
933
+ try {
934
+ chain.serializedCache.set(dataColumnSidecar, serializedData);
935
+ const blockInputMeta = blockInput.getLogMeta();
936
+ const {receivedColumns} = blockInputMeta;
937
+ // it's not helpful to track every single column received
938
+ // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
939
+ switch (receivedColumns) {
940
+ case 1:
941
+ case config.SAMPLES_PER_SLOT:
942
+ case 2 * config.SAMPLES_PER_SLOT:
943
+ case NUMBER_OF_COLUMNS / 4:
944
+ case NUMBER_OF_COLUMNS / 2:
945
+ case NUMBER_OF_COLUMNS:
946
+ metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec);
947
+ break;
948
+ }
905
949
 
906
- if (!blockInput.hasComputedAllData()) {
907
- // immediately attempt fetch of data columns from execution engine
908
- chain.getBlobsTracker.triggerGetBlobs(blockInput);
909
- // if we've received at least half of the columns, trigger reconstruction of the rest
910
- if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) {
911
- chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput);
912
- }
913
- }
950
+ if (!blockInput.hasComputedAllData()) {
951
+ // immediately attempt fetch of data columns from execution engine
952
+ chain.getBlobsTracker.triggerGetBlobs(blockInput);
953
+ // if we've received at least half of the columns, trigger reconstruction of the rest
954
+ if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) {
955
+ chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput);
956
+ }
957
+ }
914
958
 
915
- if (!blockInput.hasBlockAndAllData()) {
916
- const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
917
- chain.logger.debug("Received gossip data column, waiting for full data availability", {
918
- msToWait: cutoffTimeMs,
919
- dataColumnIndex: index,
920
- ...blockInputMeta,
921
- });
922
- // do not await here to not delay gossip validation
923
- blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => {
924
- chain.logger.debug(
925
- "Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput",
926
- {
959
+ if (!blockInput.hasBlockAndAllData()) {
960
+ const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
961
+ chain.logger.debug("Received gossip data column, waiting for full data availability", {
962
+ msToWait: cutoffTimeMs,
927
963
  dataColumnIndex: index,
928
964
  ...blockInputMeta,
929
- }
965
+ });
966
+ // do not await here to not delay gossip validation
967
+ blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => {
968
+ chain.logger.debug(
969
+ "Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput",
970
+ {
971
+ dataColumnIndex: index,
972
+ ...blockInputMeta,
973
+ }
974
+ );
975
+ chain.emitter.emit(ChainEvent.incompleteBlockInput, {
976
+ blockInput,
977
+ peer: peerIdStr,
978
+ source: BlockInputSource.gossip,
979
+ });
980
+ });
981
+ }
982
+ } catch (e) {
983
+ logger.debug(
984
+ "Error handling gossip data column",
985
+ {slot: dataColumnSlot, root: blockInput.blockRootHex, index},
986
+ e as Error
930
987
  );
931
- chain.emitter.emit(ChainEvent.incompleteBlockInput, {
932
- blockInput,
933
- peer: peerIdStr,
934
- source: BlockInputSource.gossip,
935
- });
936
- });
937
- }
988
+ }
989
+ });
938
990
  }
939
991
  },
940
992
 
@@ -961,36 +1013,41 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
961
1013
  throw e;
962
1014
  }
963
1015
 
964
- // Handler
1016
+ // Handler - deferred to next event loop so the validation result propagates first
965
1017
  const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = validationResult;
966
- chain.validatorMonitor?.registerGossipAggregatedAttestation(
967
- seenTimestampSec,
968
- signedAggregateAndProof,
969
- indexedAttestation
970
- );
971
- const aggregatedAttestation = signedAggregateAndProof.message.aggregate;
1018
+ callInNextEventLoop(() => {
1019
+ try {
1020
+ chain.validatorMonitor?.registerGossipAggregatedAttestation(
1021
+ seenTimestampSec,
1022
+ signedAggregateAndProof,
1023
+ indexedAttestation
1024
+ );
1025
+ const aggregatedAttestation = signedAggregateAndProof.message.aggregate;
972
1026
 
973
- const insertOutcome = chain.aggregatedAttestationPool.add(
974
- aggregatedAttestation,
975
- attDataRootHex,
976
- indexedAttestation.attestingIndices.length,
977
- committeeValidatorIndices
978
- );
979
- metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1027
+ const insertOutcome = chain.aggregatedAttestationPool.add(
1028
+ aggregatedAttestation,
1029
+ attDataRootHex,
1030
+ indexedAttestation.attestingIndices.length,
1031
+ committeeValidatorIndices
1032
+ );
1033
+ metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({insertOutcome});
980
1034
 
981
- if (!options.dontSendGossipAttestationsToForkchoice) {
982
- try {
983
- chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1035
+ if (!options.dontSendGossipAttestationsToForkchoice) {
1036
+ chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1037
+ }
1038
+
1039
+ chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate);
984
1040
  } catch (e) {
985
1041
  logger.debug(
986
- "Error adding gossip aggregated attestation to forkchoice",
987
- {slot: aggregatedAttestation.data.slot},
1042
+ "Error handling gossip aggregate and proof",
1043
+ {
1044
+ slot: signedAggregateAndProof.message.aggregate.data.slot,
1045
+ root: toRootHex(signedAggregateAndProof.message.aggregate.data.beaconBlockRoot),
1046
+ },
988
1047
  e as Error
989
1048
  );
990
1049
  }
991
- }
992
-
993
- chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate);
1050
+ });
994
1051
  },
995
1052
 
996
1053
  [GossipType.attester_slashing]: async ({
@@ -1002,16 +1059,20 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1002
1059
  const attesterSlashing = sszDeserialize(topic, serializedData);
1003
1060
  await validateGossipAttesterSlashing(chain, attesterSlashing);
1004
1061
 
1005
- // Handler
1006
-
1007
- try {
1008
- chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
1009
- chain.forkChoice.onAttesterSlashing(attesterSlashing);
1010
- } catch (e) {
1011
- logger.error("Error adding attesterSlashing to pool", {}, e as Error);
1012
- }
1013
-
1014
- chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
1062
+ // Handler - deferred to next event loop so the validation result propagates first
1063
+ callInNextEventLoop(() => {
1064
+ try {
1065
+ chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
1066
+ chain.forkChoice.onAttesterSlashing(attesterSlashing);
1067
+ chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
1068
+ } catch (e) {
1069
+ logger.debug(
1070
+ "Error handling gossip attester slashing",
1071
+ {slot: attesterSlashing.attestation1.data.slot},
1072
+ e as Error
1073
+ );
1074
+ }
1075
+ });
1015
1076
  },
1016
1077
 
1017
1078
  [GossipType.proposer_slashing]: async ({
@@ -1022,15 +1083,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1022
1083
  const proposerSlashing = sszDeserialize(topic, serializedData);
1023
1084
  await validateGossipProposerSlashing(chain, proposerSlashing);
1024
1085
 
1025
- // Handler
1026
-
1027
- try {
1028
- chain.opPool.insertProposerSlashing(proposerSlashing);
1029
- } catch (e) {
1030
- logger.error("Error adding attesterSlashing to pool", {}, e as Error);
1031
- }
1032
-
1033
- chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
1086
+ // Handler - deferred to next event loop so the validation result propagates first
1087
+ callInNextEventLoop(() => {
1088
+ try {
1089
+ chain.opPool.insertProposerSlashing(proposerSlashing);
1090
+ chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
1091
+ } catch (e) {
1092
+ logger.debug(
1093
+ "Error handling gossip proposer slashing",
1094
+ {
1095
+ slot: proposerSlashing.signedHeader1.message.slot,
1096
+ proposerIndex: proposerSlashing.signedHeader1.message.proposerIndex,
1097
+ },
1098
+ e as Error
1099
+ );
1100
+ }
1101
+ });
1034
1102
  },
1035
1103
 
1036
1104
  [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric<GossipType.voluntary_exit>) => {
@@ -1038,15 +1106,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1038
1106
  const voluntaryExit = sszDeserialize(topic, serializedData);
1039
1107
  await validateGossipVoluntaryExit(chain, voluntaryExit);
1040
1108
 
1041
- // Handler
1042
-
1043
- try {
1044
- chain.opPool.insertVoluntaryExit(voluntaryExit);
1045
- } catch (e) {
1046
- logger.error("Error adding voluntaryExit to pool", {}, e as Error);
1047
- }
1048
-
1049
- chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit);
1109
+ // Handler - deferred to next event loop so the validation result propagates first
1110
+ callInNextEventLoop(() => {
1111
+ try {
1112
+ chain.opPool.insertVoluntaryExit(voluntaryExit);
1113
+ chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit);
1114
+ } catch (e) {
1115
+ logger.debug(
1116
+ "Error handling gossip voluntary exit",
1117
+ {epoch: voluntaryExit.message.epoch, validatorIndex: voluntaryExit.message.validatorIndex},
1118
+ e as Error
1119
+ );
1120
+ }
1121
+ });
1050
1122
  },
1051
1123
 
1052
1124
  [GossipType.sync_committee_contribution_and_proof]: async ({
@@ -1065,22 +1137,30 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1065
1137
  throw e;
1066
1138
  });
1067
1139
 
1068
- // Handler
1069
- chain.validatorMonitor?.registerGossipSyncContributionAndProof(
1070
- contributionAndProof.message,
1071
- syncCommitteeParticipantIndices
1072
- );
1073
- try {
1074
- const insertOutcome = chain.syncContributionAndProofPool.add(
1075
- contributionAndProof.message,
1076
- syncCommitteeParticipantIndices.length
1077
- );
1078
- metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({insertOutcome});
1079
- } catch (e) {
1080
- logger.error("Error adding to contributionAndProof pool", {}, e as Error);
1081
- }
1082
-
1083
- chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof);
1140
+ // Handler - deferred to next event loop so the validation result propagates first
1141
+ callInNextEventLoop(() => {
1142
+ try {
1143
+ chain.validatorMonitor?.registerGossipSyncContributionAndProof(
1144
+ contributionAndProof.message,
1145
+ syncCommitteeParticipantIndices
1146
+ );
1147
+ const insertOutcome = chain.syncContributionAndProofPool.add(
1148
+ contributionAndProof.message,
1149
+ syncCommitteeParticipantIndices.length
1150
+ );
1151
+ metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({insertOutcome});
1152
+ chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof);
1153
+ } catch (e) {
1154
+ logger.debug(
1155
+ "Error handling gossip contribution and proof",
1156
+ {
1157
+ slot: contributionAndProof.message.contribution.slot,
1158
+ subcommitteeIndex: contributionAndProof.message.contribution.subcommitteeIndex,
1159
+ },
1160
+ e as Error
1161
+ );
1162
+ }
1163
+ });
1084
1164
  },
1085
1165
 
1086
1166
  [GossipType.sync_committee]: async ({gossipData, topic}: GossipHandlerParamGeneric<GossipType.sync_committee>) => {
@@ -1097,15 +1177,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1097
1177
  throw e;
1098
1178
  }
1099
1179
 
1100
- // Handler — add for ALL positions this validator holds in the subcommittee
1101
- try {
1102
- for (const indexInSubcommittee of indicesInSubcommittee) {
1103
- const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
1104
- metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome});
1180
+ // Handler - deferred to next event loop so the validation result propagates first
1181
+ // add for ALL positions this validator holds in the subcommittee
1182
+ callInNextEventLoop(() => {
1183
+ try {
1184
+ for (const indexInSubcommittee of indicesInSubcommittee) {
1185
+ const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
1186
+ metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome});
1187
+ }
1188
+ } catch (e) {
1189
+ logger.debug(
1190
+ "Error handling gossip sync committee",
1191
+ {slot: syncCommittee.slot, subnet, validatorIndex: syncCommittee.validatorIndex},
1192
+ e as Error
1193
+ );
1105
1194
  }
1106
- } catch (e) {
1107
- logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error);
1108
- }
1195
+ });
1109
1196
  },
1110
1197
 
1111
1198
  [GossipType.light_client_finality_update]: async ({
@@ -1135,14 +1222,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1135
1222
  const blsToExecutionChange = sszDeserialize(topic, serializedData);
1136
1223
  await validateGossipBlsToExecutionChange(chain, blsToExecutionChange);
1137
1224
 
1138
- // Handler
1139
- try {
1140
- chain.opPool.insertBlsToExecutionChange(blsToExecutionChange);
1141
- } catch (e) {
1142
- logger.error("Error adding blsToExecutionChange to pool", {}, e as Error);
1143
- }
1144
-
1145
- chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange);
1225
+ // Handler - deferred to next event loop so the validation result propagates first
1226
+ callInNextEventLoop(() => {
1227
+ try {
1228
+ chain.opPool.insertBlsToExecutionChange(blsToExecutionChange);
1229
+ chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange);
1230
+ } catch (e) {
1231
+ logger.debug(
1232
+ "Error handling gossip bls to execution change",
1233
+ {validatorIndex: blsToExecutionChange.message.validatorIndex},
1234
+ e as Error
1235
+ );
1236
+ }
1237
+ });
1146
1238
  },
1147
1239
  [GossipType.execution_payload]: async ({
1148
1240
  gossipData,
@@ -1203,57 +1295,64 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1203
1295
  });
1204
1296
  }
1205
1297
 
1206
- chain.serializedCache.set(signedEnvelope, serializedData);
1207
-
1208
- payloadInput.addPayloadEnvelope({
1209
- envelope: signedEnvelope,
1210
- source: PayloadEnvelopeInputSource.gossip,
1211
- seenTimestampSec,
1212
- peerIdStr,
1213
- });
1298
+ // Handler - deferred to next event loop so the validation result propagates first
1299
+ callInNextEventLoop(() => {
1300
+ try {
1301
+ chain.serializedCache.set(signedEnvelope, serializedData);
1214
1302
 
1215
- chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
1216
- slot,
1217
- builderIndex: envelope.builderIndex,
1218
- blockHash: toRootHex(envelope.payload.blockHash),
1219
- blockRoot: blockRootHex,
1220
- });
1303
+ payloadInput.addPayloadEnvelope({
1304
+ envelope: signedEnvelope,
1305
+ source: PayloadEnvelopeInputSource.gossip,
1306
+ seenTimestampSec,
1307
+ peerIdStr,
1308
+ });
1221
1309
 
1222
- chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1223
- // Adjust verbosity based on error type
1224
- let logLevel: LogLevel;
1310
+ chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
1311
+ slot,
1312
+ builderIndex: envelope.builderIndex,
1313
+ blockHash: toRootHex(envelope.payload.blockHash),
1314
+ blockRoot: blockRootHex,
1315
+ });
1225
1316
 
1226
- if (e instanceof PayloadError) {
1227
- switch (e.type.code) {
1228
- // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
1229
- // already verified the block is in fork choice
1230
- case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
1231
- case PayloadErrorCode.MISS_BLOCK_STATE:
1232
- case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1233
- // Errors might indicate an issue with our node or the connected EL client
1317
+ chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
1318
+ // Adjust verbosity based on error type
1319
+ let logLevel: LogLevel;
1320
+
1321
+ if (e instanceof PayloadError) {
1322
+ switch (e.type.code) {
1323
+ // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
1324
+ // already verified the block is in fork choice
1325
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
1326
+ case PayloadErrorCode.MISS_BLOCK_STATE:
1327
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
1328
+ // Errors might indicate an issue with our node or the connected EL client
1329
+ logLevel = LogLevel.error;
1330
+ break;
1331
+ // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1332
+ case PayloadErrorCode.INVALID_SIGNATURE:
1333
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1334
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1335
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1336
+ // Misbehaving peer, but could highlight an issue in another client
1337
+ logLevel = LogLevel.warn;
1338
+ break;
1339
+ }
1340
+ } else {
1341
+ // Any unexpected error
1234
1342
  logLevel = LogLevel.error;
1235
- break;
1236
- // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
1237
- case PayloadErrorCode.INVALID_SIGNATURE:
1238
- case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
1239
- case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
1240
- core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
1241
- // Misbehaving peer, but could highlight an issue in another client
1242
- logLevel = LogLevel.warn;
1243
- break;
1244
- }
1245
- } else {
1246
- // Any unexpected error
1247
- logLevel = LogLevel.error;
1343
+ }
1344
+ metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
1345
+ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
1346
+ });
1347
+ chain.logger[logLevel](
1348
+ "Error processing execution payload from gossip",
1349
+ {slot, root: blockRootHex, peer: peerIdStr},
1350
+ e as Error
1351
+ );
1352
+ });
1353
+ } catch (e) {
1354
+ logger.debug("Error handling gossip execution payload", {slot, root: blockRootHex}, e as Error);
1248
1355
  }
1249
- metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
1250
- error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
1251
- });
1252
- chain.logger[logLevel](
1253
- "Error processing execution payload from gossip",
1254
- {slot, peer: peerIdStr, root: blockRootHex},
1255
- e as Error
1256
- );
1257
1356
  });
1258
1357
  },
1259
1358
  [GossipType.payload_attestation_message]: async ({
@@ -1265,30 +1364,49 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1265
1364
  const payloadAttestationMessage = sszDeserialize(topic, serializedData);
1266
1365
  const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage);
1267
1366
 
1268
- const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1269
- metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1367
+ // Handler - deferred to next event loop so the validation result propagates first
1368
+ callInNextEventLoop(() => {
1369
+ try {
1370
+ const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1371
+ metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1372
+ const nextSlotProposer = chain.getHeadState().getBeaconProposer(payloadAttestationMessage.data.slot + 1);
1373
+ const isNextSlotProposer = chain.beaconProposerCache.get(nextSlotProposer) !== undefined;
1374
+
1375
+ if (isNextSlotProposer) {
1376
+ try {
1377
+ const insertOutcome = chain.payloadAttestationPool.add(
1378
+ payloadAttestationMessage,
1379
+ validationResult.attDataRootHex,
1380
+ validationResult.validatorCommitteeIndices
1381
+ );
1382
+ metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1383
+ } catch (e) {
1384
+ logger.debug("Error adding to payloadAttestation pool", {}, e as Error);
1385
+ }
1386
+ }
1270
1387
 
1271
- try {
1272
- const insertOutcome = chain.payloadAttestationPool.add(
1273
- payloadAttestationMessage,
1274
- validationResult.attDataRootHex,
1275
- validationResult.validatorCommitteeIndices
1276
- );
1277
- metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1278
- } catch (e) {
1279
- logger.error("Error adding to payloadAttestation pool", {}, e as Error);
1280
- }
1281
- chain.forkChoice.notifyPtcMessages(
1282
- toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1283
- payloadAttestationMessage.data.slot,
1284
- validationResult.validatorCommitteeIndices,
1285
- payloadAttestationMessage.data.payloadPresent,
1286
- payloadAttestationMessage.data.blobDataAvailable
1287
- );
1388
+ chain.forkChoice.notifyPtcMessages(
1389
+ toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1390
+ payloadAttestationMessage.data.slot,
1391
+ validationResult.validatorCommitteeIndices,
1392
+ payloadAttestationMessage.data.payloadPresent,
1393
+ payloadAttestationMessage.data.blobDataAvailable
1394
+ );
1288
1395
 
1289
- chain.emitter.emit(routes.events.EventType.payloadAttestationMessage, {
1290
- version: config.getForkName(payloadAttestationMessage.data.slot),
1291
- data: payloadAttestationMessage,
1396
+ chain.emitter.emit(routes.events.EventType.payloadAttestationMessage, {
1397
+ version: config.getForkName(payloadAttestationMessage.data.slot),
1398
+ data: payloadAttestationMessage,
1399
+ });
1400
+ } catch (e) {
1401
+ logger.debug(
1402
+ "Error handling gossip payload attestation message",
1403
+ {
1404
+ slot: payloadAttestationMessage.data.slot,
1405
+ root: toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1406
+ },
1407
+ e as Error
1408
+ );
1409
+ }
1292
1410
  });
1293
1411
  },
1294
1412
  [GossipType.execution_payload_bid]: async ({
@@ -1300,23 +1418,38 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1300
1418
  const executionPayloadBid = sszDeserialize(topic, serializedData);
1301
1419
  const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
1302
1420
 
1303
- // this could be negative, because it's most likely the bid of next slot comes at this clock slot
1304
- const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
1305
- metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.gossip}, elapsedSec);
1306
-
1307
- // Handle valid payload bid by storing in a bid pool
1308
- try {
1309
- const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid, Math.floor(elapsedSec * 1000));
1310
- metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome});
1311
- } catch (e) {
1312
- logger.error("Error adding to executionPayloadBid pool", {}, e as Error);
1313
- }
1314
-
1315
- chain.validatorMonitor?.registerExecutionPayloadBid(OpSource.gossip, proposerIndex, executionPayloadBid.message);
1421
+ // Handler - deferred to next event loop so the validation result propagates first
1422
+ callInNextEventLoop(() => {
1423
+ try {
1424
+ // this could be negative, because it's most likely the bid of next slot comes at this clock slot
1425
+ const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
1426
+ metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.gossip}, elapsedSec);
1427
+
1428
+ // Handle valid payload bid by storing in a bid pool
1429
+ const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid, Math.floor(elapsedSec * 1000));
1430
+ metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome});
1431
+
1432
+ chain.validatorMonitor?.registerExecutionPayloadBid(
1433
+ OpSource.gossip,
1434
+ proposerIndex,
1435
+ executionPayloadBid.message
1436
+ );
1316
1437
 
1317
- chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
1318
- version: config.getForkName(executionPayloadBid.message.slot),
1319
- data: executionPayloadBid,
1438
+ chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
1439
+ version: config.getForkName(executionPayloadBid.message.slot),
1440
+ data: executionPayloadBid,
1441
+ });
1442
+ } catch (e) {
1443
+ logger.debug(
1444
+ "Error handling gossip execution payload bid",
1445
+ {
1446
+ slot: executionPayloadBid.message.slot,
1447
+ root: toRootHex(executionPayloadBid.message.parentBlockRoot),
1448
+ proposerIndex,
1449
+ },
1450
+ e as Error
1451
+ );
1452
+ }
1320
1453
  });
1321
1454
  },
1322
1455
  [GossipType.proposer_preferences]: async ({
@@ -1327,9 +1460,20 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1327
1460
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
1328
1461
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
1329
1462
 
1330
- chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1331
- version: config.getForkName(signedProposerPreferences.message.proposalSlot),
1332
- data: signedProposerPreferences,
1463
+ // Handler - deferred to next event loop so the validation result propagates first
1464
+ callInNextEventLoop(() => {
1465
+ try {
1466
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1467
+ version: config.getForkName(signedProposerPreferences.message.proposalSlot),
1468
+ data: signedProposerPreferences,
1469
+ });
1470
+ } catch (e) {
1471
+ logger.debug(
1472
+ "Error handling gossip proposer preferences",
1473
+ {proposalSlot: signedProposerPreferences.message.proposalSlot},
1474
+ e as Error
1475
+ );
1476
+ }
1333
1477
  });
1334
1478
  },
1335
1479
  };
@@ -1363,69 +1507,9 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
1363
1507
  chain,
1364
1508
  validationParams
1365
1509
  );
1366
- for (const [i, validationResult] of validationResults.entries()) {
1367
- if (validationResult.err) {
1368
- results.push(validationResult.err as AttestationError);
1369
- continue;
1370
- }
1371
- // null means no error
1372
- results.push(null);
1373
-
1374
- // Handler
1375
- const {
1376
- indexedAttestation,
1377
- attDataRootHex,
1378
- attestation,
1379
- committeeIndex,
1380
- validatorCommitteeIndex,
1381
- committeeSize,
1382
- } = validationResult.result;
1383
- chain.validatorMonitor?.registerGossipUnaggregatedAttestation(
1384
- gossipHandlerParams[i].seenTimestampSec,
1385
- indexedAttestation
1386
- );
1387
-
1388
- const {subnet} = validationResult.result;
1389
- try {
1390
- // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages
1391
- // but don't add to attestation pool, to save CPU and RAM
1392
- if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) {
1393
- const insertOutcome = chain.attestationPool.add(
1394
- committeeIndex,
1395
- attestation,
1396
- attDataRootHex,
1397
- validatorCommitteeIndex,
1398
- committeeSize
1399
- );
1400
- metrics?.opPool.attestationPool.gossipInsertOutcome.inc({insertOutcome});
1401
- }
1402
- } catch (e) {
1403
- logger.error("Error adding unaggregated attestation to pool", {subnet}, e as Error);
1404
- }
1405
-
1406
- if (!options.dontSendGossipAttestationsToForkchoice) {
1407
- try {
1408
- chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1409
- } catch (e) {
1410
- logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error);
1411
- }
1412
- }
1413
-
1414
- if (isForkPostElectra(fork)) {
1415
- chain.emitter.emit(
1416
- routes.events.EventType.singleAttestation,
1417
- attestation as SingleAttestation<ForkPostElectra>
1418
- );
1419
- } else {
1420
- chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation<ForkPreElectra>);
1421
- chain.emitter.emit(
1422
- routes.events.EventType.singleAttestation,
1423
- toElectraSingleAttestation(
1424
- attestation as SingleAttestation<ForkPreElectra>,
1425
- indexedAttestation.attestingIndices[0]
1426
- )
1427
- );
1428
- }
1510
+ // verdicts computed synchronously and returned immediately
1511
+ for (const validationResult of validationResults) {
1512
+ results.push(validationResult.err ? (validationResult.err as AttestationError) : null);
1429
1513
  }
1430
1514
 
1431
1515
  if (batchableBls) {
@@ -1434,6 +1518,72 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
1434
1518
  metrics?.gossipAttestation.attestationNonBatchCount.inc(attestationCount);
1435
1519
  }
1436
1520
 
1521
+ // Handler - deferred to next event loop so the validation result propagates first.
1522
+ callInNextEventLoop(() => {
1523
+ for (const [i, validationResult] of validationResults.entries()) {
1524
+ if (validationResult.err) continue;
1525
+ const {
1526
+ indexedAttestation,
1527
+ attDataRootHex,
1528
+ attestation,
1529
+ committeeIndex,
1530
+ validatorCommitteeIndex,
1531
+ committeeSize,
1532
+ subnet,
1533
+ } = validationResult.result;
1534
+ try {
1535
+ chain.validatorMonitor?.registerGossipUnaggregatedAttestation(
1536
+ gossipHandlerParams[i].seenTimestampSec,
1537
+ indexedAttestation
1538
+ );
1539
+
1540
+ try {
1541
+ // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages
1542
+ // but don't add to attestation pool, to save CPU and RAM
1543
+ if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) {
1544
+ const insertOutcome = chain.attestationPool.add(
1545
+ committeeIndex,
1546
+ attestation,
1547
+ attDataRootHex,
1548
+ validatorCommitteeIndex,
1549
+ committeeSize
1550
+ );
1551
+ metrics?.opPool.attestationPool.gossipInsertOutcome.inc({insertOutcome});
1552
+ }
1553
+ } catch (e) {
1554
+ logger.debug("Error adding gossip unaggregated attestation to pool", {subnet}, e as Error);
1555
+ }
1556
+
1557
+ // Separate boundary: a pool insertion error above must not skip the fork-choice vote
1558
+ if (!options.dontSendGossipAttestationsToForkchoice) {
1559
+ try {
1560
+ chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1561
+ } catch (e) {
1562
+ logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error);
1563
+ }
1564
+ }
1565
+
1566
+ if (isForkPostElectra(fork)) {
1567
+ chain.emitter.emit(
1568
+ routes.events.EventType.singleAttestation,
1569
+ attestation as SingleAttestation<ForkPostElectra>
1570
+ );
1571
+ } else {
1572
+ chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation<ForkPreElectra>);
1573
+ chain.emitter.emit(
1574
+ routes.events.EventType.singleAttestation,
1575
+ toElectraSingleAttestation(
1576
+ attestation as SingleAttestation<ForkPreElectra>,
1577
+ indexedAttestation.attestingIndices[0]
1578
+ )
1579
+ );
1580
+ }
1581
+ } catch (e) {
1582
+ logger.debug("Error handling gossip attestation", {slot: indexedAttestation.data.slot, subnet}, e as Error);
1583
+ }
1584
+ }
1585
+ });
1586
+
1437
1587
  return results;
1438
1588
  },
1439
1589
  };