@lodestar/beacon-node 1.48.0-dev.b24dce51bf → 1.48.0-dev.baa28132d2

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 (90) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +0 -7
  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/chain/archiveStore/archiveStore.d.ts.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.js +4 -1
  12. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  13. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +9 -1
  14. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/utils/archiveBlocks.js +37 -4
  16. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  17. package/lib/chain/blocks/importBlock.d.ts +5 -1
  18. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  19. package/lib/chain/blocks/importBlock.js +12 -5
  20. package/lib/chain/blocks/importBlock.js.map +1 -1
  21. package/lib/chain/blocks/index.d.ts +8 -4
  22. package/lib/chain/blocks/index.d.ts.map +1 -1
  23. package/lib/chain/blocks/index.js +80 -15
  24. package/lib/chain/blocks/index.js.map +1 -1
  25. package/lib/chain/blocks/types.d.ts +8 -0
  26. package/lib/chain/blocks/types.d.ts.map +1 -1
  27. package/lib/chain/blocks/types.js.map +1 -1
  28. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  29. package/lib/chain/blocks/utils/chainSegment.js +16 -10
  30. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  31. package/lib/chain/chain.d.ts +2 -2
  32. package/lib/chain/chain.d.ts.map +1 -1
  33. package/lib/chain/chain.js +2 -2
  34. package/lib/chain/chain.js.map +1 -1
  35. package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
  36. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  37. package/lib/chain/errors/executionPayloadBid.js +1 -0
  38. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  39. package/lib/chain/errors/gossipValidation.d.ts +2 -1
  40. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  41. package/lib/chain/errors/gossipValidation.js +1 -0
  42. package/lib/chain/errors/gossipValidation.js.map +1 -1
  43. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  44. package/lib/chain/forkChoice/index.js +2 -0
  45. package/lib/chain/forkChoice/index.js.map +1 -1
  46. package/lib/chain/interface.d.ts +2 -2
  47. package/lib/chain/interface.d.ts.map +1 -1
  48. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  49. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  50. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  51. package/lib/chain/validation/executionPayloadBid.js +16 -0
  52. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  53. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  54. package/lib/execution/builder/validateBid.js +3 -0
  55. package/lib/execution/builder/validateBid.js.map +1 -1
  56. package/lib/metrics/metrics/lodestar.d.ts +4 -3
  57. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  58. package/lib/metrics/metrics/lodestar.js +5 -5
  59. package/lib/metrics/metrics/lodestar.js.map +1 -1
  60. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  61. package/lib/network/processor/gossipHandlers.js +378 -278
  62. package/lib/network/processor/gossipHandlers.js.map +1 -1
  63. package/lib/network/processor/index.d.ts.map +1 -1
  64. package/lib/network/processor/index.js +22 -16
  65. package/lib/network/processor/index.js.map +1 -1
  66. package/lib/sync/range/range.d.ts.map +1 -1
  67. package/lib/sync/range/range.js +24 -1
  68. package/lib/sync/range/range.js.map +1 -1
  69. package/package.json +14 -14
  70. package/src/api/impl/beacon/blocks/index.ts +0 -10
  71. package/src/api/impl/beacon/state/utils.ts +15 -0
  72. package/src/api/impl/config/index.ts +4 -1
  73. package/src/chain/archiveStore/archiveStore.ts +6 -0
  74. package/src/chain/archiveStore/utils/archiveBlocks.ts +39 -3
  75. package/src/chain/blocks/importBlock.ts +25 -7
  76. package/src/chain/blocks/index.ts +107 -21
  77. package/src/chain/blocks/types.ts +9 -0
  78. package/src/chain/blocks/utils/chainSegment.ts +16 -11
  79. package/src/chain/chain.ts +4 -4
  80. package/src/chain/errors/executionPayloadBid.ts +2 -0
  81. package/src/chain/errors/gossipValidation.ts +1 -0
  82. package/src/chain/forkChoice/index.ts +2 -0
  83. package/src/chain/interface.ts +2 -2
  84. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  85. package/src/chain/validation/executionPayloadBid.ts +18 -0
  86. package/src/execution/builder/validateBid.ts +4 -0
  87. package/src/metrics/metrics/lodestar.ts +6 -5
  88. package/src/network/processor/gossipHandlers.ts +487 -348
  89. package/src/network/processor/index.ts +24 -17
  90. package/src/sync/range/range.ts +23 -1
@@ -22,6 +22,7 @@ import { validateGossipProposerPreferences } from "../../chain/validation/propos
22
22
  import { OpSource } from "../../chain/validatorMonitor.js";
23
23
  import { kzgCommitmentToVersionedHash } from "../../util/blobs.js";
24
24
  import { getBlobKzgCommitments, getDataColumnSidecarSlot } from "../../util/dataColumns.js";
25
+ import { callInNextEventLoop } from "../../util/eventLoop.js";
25
26
  import { GossipType, } from "../gossip/interface.js";
26
27
  import { sszDeserialize } from "../gossip/topic.js";
27
28
  import { PeerAction } from "../peers/index.js";
@@ -520,8 +521,16 @@ function getSequentialHandlers(modules, options) {
520
521
  const signedBlock = sszDeserialize(topic, serializedData);
521
522
  try {
522
523
  const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
523
- chain.serializedCache.set(signedBlock, serializedData);
524
- handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
524
+ // Handler - deferred to next event loop so the validation result propagates first
525
+ callInNextEventLoop(() => {
526
+ try {
527
+ chain.serializedCache.set(signedBlock, serializedData);
528
+ handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
529
+ }
530
+ catch (e) {
531
+ logger.debug("Error handling gossip block", { slot: signedBlock.message.slot, root: blockInput.blockRootHex }, e);
532
+ }
533
+ });
525
534
  }
526
535
  catch (e) {
527
536
  // Spec: IGNORE the block, ie not to re-publish to peers
@@ -534,6 +543,8 @@ function getSequentialHandlers(modules, options) {
534
543
  // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
535
544
  const blockInput = chain.seenBlockInputCache.get(e.type.root);
536
545
  if (blockInput) {
546
+ // We're returning IGNORE (thrown below), so this block is not forwarded to peers. Unlike the
547
+ // happy path there is no rush to forward, so we don't need to wrap in callInNextEventLoop.
537
548
  chain.serializedCache.set(signedBlock, serializedData);
538
549
  // this is technically not a valid gossip block but gossip validation is a cheap subset of checks
539
550
  // this runs the full state transition, so importing an equivocating-but-valid block here is safe.
@@ -553,26 +564,34 @@ function getSequentialHandlers(modules, options) {
553
564
  throw new GossipActionError(GossipAction.REJECT, { code: "PRE_DENEB_BLOCK" });
554
565
  }
555
566
  const blockInput = await validateBeaconBlob(blobSidecar, topic.subnet, peerIdStr, seenTimestampSec);
556
- chain.serializedCache.set(blobSidecar, serializedData);
557
- if (!blockInput.hasBlockAndAllData()) {
558
- const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
559
- chain.logger.debug("Received gossip blob, waiting for full data availability", {
560
- msToWait: cutoffTimeMs,
561
- blobIndex: index,
562
- ...blockInput.getLogMeta(),
563
- });
564
- blockInput.waitForAllData(cutoffTimeMs).catch((_e) => {
565
- chain.logger.debug("Waited for data after receiving gossip blob. Cut-off reached so attempting to fetch remainder of BlockInput", {
566
- blobIndex: index,
567
- ...blockInput.getLogMeta(),
568
- });
569
- chain.emitter.emit(ChainEvent.incompleteBlockInput, {
570
- blockInput,
571
- peer: peerIdStr,
572
- source: BlockInputSource.gossip,
573
- });
574
- });
575
- }
567
+ // Handler - deferred to next event loop so the validation result propagates first
568
+ callInNextEventLoop(() => {
569
+ try {
570
+ chain.serializedCache.set(blobSidecar, serializedData);
571
+ if (!blockInput.hasBlockAndAllData()) {
572
+ const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
573
+ chain.logger.debug("Received gossip blob, waiting for full data availability", {
574
+ msToWait: cutoffTimeMs,
575
+ blobIndex: index,
576
+ ...blockInput.getLogMeta(),
577
+ });
578
+ blockInput.waitForAllData(cutoffTimeMs).catch((_e) => {
579
+ chain.logger.debug("Waited for data after receiving gossip blob. Cut-off reached so attempting to fetch remainder of BlockInput", {
580
+ blobIndex: index,
581
+ ...blockInput.getLogMeta(),
582
+ });
583
+ chain.emitter.emit(ChainEvent.incompleteBlockInput, {
584
+ blockInput,
585
+ peer: peerIdStr,
586
+ source: BlockInputSource.gossip,
587
+ });
588
+ });
589
+ }
590
+ }
591
+ catch (e) {
592
+ logger.debug("Error handling gossip blob", { slot: blobSlot, root: blockInput.blockRootHex, index }, e);
593
+ }
594
+ });
576
595
  },
577
596
  [GossipType.data_column_sidecar]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }) => {
578
597
  const { fork } = topic.boundary;
@@ -592,50 +611,58 @@ function getSequentialHandlers(modules, options) {
592
611
  }
593
612
  // After gloas, data columns are tracked in PayloadEnvelopeInput
594
613
  const payloadInput = await validatePayloadDataColumn(dataColumnSidecar, topic.subnet, peerIdStr, seenTimestampSec);
595
- chain.serializedCache.set(dataColumnSidecar, serializedData);
596
- const payloadInputMeta = payloadInput.getLogMeta();
597
- const { receivedColumns } = payloadInputMeta;
598
- // it's not helpful to track every single column received
599
- // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
600
- switch (receivedColumns) {
601
- case 1:
602
- case config.SAMPLES_PER_SLOT:
603
- case 2 * config.SAMPLES_PER_SLOT:
604
- case NUMBER_OF_COLUMNS / 4:
605
- case NUMBER_OF_COLUMNS / 2:
606
- case NUMBER_OF_COLUMNS:
607
- metrics?.dataColumns.elapsedTimeTillReceived.observe({ receivedOrder: receivedColumns }, delaySec);
608
- break;
609
- }
610
- if (!payloadInput.hasComputedAllData()) {
611
- // if we've received at least half of the columns, trigger reconstruction of the rest
612
- if (receivedColumns >= NUMBER_OF_COLUMNS / 2) {
613
- chain.columnReconstructionTracker.triggerColumnReconstruction(payloadInput);
614
+ // Handler - deferred to next event loop so the validation result propagates first
615
+ callInNextEventLoop(() => {
616
+ try {
617
+ chain.serializedCache.set(dataColumnSidecar, serializedData);
618
+ const payloadInputMeta = payloadInput.getLogMeta();
619
+ const { receivedColumns } = payloadInputMeta;
620
+ // it's not helpful to track every single column received
621
+ // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
622
+ switch (receivedColumns) {
623
+ case 1:
624
+ case config.SAMPLES_PER_SLOT:
625
+ case 2 * config.SAMPLES_PER_SLOT:
626
+ case NUMBER_OF_COLUMNS / 4:
627
+ case NUMBER_OF_COLUMNS / 2:
628
+ case NUMBER_OF_COLUMNS:
629
+ metrics?.dataColumns.elapsedTimeTillReceived.observe({ receivedOrder: receivedColumns }, delaySec);
630
+ break;
631
+ }
632
+ if (!payloadInput.hasComputedAllData()) {
633
+ // if we've received at least half of the columns, trigger reconstruction of the rest
634
+ if (receivedColumns >= NUMBER_OF_COLUMNS / 2) {
635
+ chain.columnReconstructionTracker.triggerColumnReconstruction(payloadInput);
636
+ }
637
+ chain.logger.debug("Received gossip data column, payload envelope input not yet complete", {
638
+ dataColumnIndex: index,
639
+ ...payloadInputMeta,
640
+ });
641
+ }
642
+ // NOTE: we do NOT call chain.processExecutionPayload here. That is triggered only by
643
+ // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
644
+ // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
645
+ if (!payloadInput.isComplete()) {
646
+ const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
647
+ // do not await here to not delay gossip validation
648
+ payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
649
+ chain.logger.debug("Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope", {
650
+ dataColumnIndex: index,
651
+ ...payloadInputMeta,
652
+ });
653
+ // TODO GLOAS: UnknownBlockSync to handle this event
654
+ chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
655
+ payloadInput,
656
+ peer: peerIdStr,
657
+ source: BlockInputSource.gossip,
658
+ });
659
+ });
660
+ }
614
661
  }
615
- chain.logger.debug("Received gossip data column, payload envelope input not yet complete", {
616
- dataColumnIndex: index,
617
- ...payloadInputMeta,
618
- });
619
- }
620
- // NOTE: we do NOT call chain.processExecutionPayload here. That is triggered only by
621
- // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
622
- // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
623
- if (!payloadInput.isComplete()) {
624
- const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
625
- // do not await here to not delay gossip validation
626
- payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
627
- chain.logger.debug("Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope", {
628
- dataColumnIndex: index,
629
- ...payloadInputMeta,
630
- });
631
- // TODO GLOAS: UnknownBlockSync to handle this event
632
- chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
633
- payloadInput,
634
- peer: peerIdStr,
635
- source: BlockInputSource.gossip,
636
- });
637
- });
638
- }
662
+ catch (e) {
663
+ logger.debug("Error handling gossip data column", { slot: dataColumnSlot, root: payloadInput.blockRootHex, index }, e);
664
+ }
665
+ });
639
666
  }
640
667
  else {
641
668
  if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) {
@@ -651,49 +678,57 @@ function getSequentialHandlers(modules, options) {
651
678
  }
652
679
  // Before gloas, data columns are tracked in BlockInput
653
680
  const blockInput = await validateBeaconDataColumn(dataColumnSidecar, serializedData, topic.subnet, peerIdStr, seenTimestampSec);
654
- chain.serializedCache.set(dataColumnSidecar, serializedData);
655
- const blockInputMeta = blockInput.getLogMeta();
656
- const { receivedColumns } = blockInputMeta;
657
- // it's not helpful to track every single column received
658
- // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
659
- switch (receivedColumns) {
660
- case 1:
661
- case config.SAMPLES_PER_SLOT:
662
- case 2 * config.SAMPLES_PER_SLOT:
663
- case NUMBER_OF_COLUMNS / 4:
664
- case NUMBER_OF_COLUMNS / 2:
665
- case NUMBER_OF_COLUMNS:
666
- metrics?.dataColumns.elapsedTimeTillReceived.observe({ receivedOrder: receivedColumns }, delaySec);
667
- break;
668
- }
669
- if (!blockInput.hasComputedAllData()) {
670
- // immediately attempt fetch of data columns from execution engine
671
- chain.getBlobsTracker.triggerGetBlobs(blockInput);
672
- // if we've received at least half of the columns, trigger reconstruction of the rest
673
- if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) {
674
- chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput);
681
+ // Handler - deferred to next event loop so the validation result propagates first
682
+ callInNextEventLoop(() => {
683
+ try {
684
+ chain.serializedCache.set(dataColumnSidecar, serializedData);
685
+ const blockInputMeta = blockInput.getLogMeta();
686
+ const { receivedColumns } = blockInputMeta;
687
+ // it's not helpful to track every single column received
688
+ // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column
689
+ switch (receivedColumns) {
690
+ case 1:
691
+ case config.SAMPLES_PER_SLOT:
692
+ case 2 * config.SAMPLES_PER_SLOT:
693
+ case NUMBER_OF_COLUMNS / 4:
694
+ case NUMBER_OF_COLUMNS / 2:
695
+ case NUMBER_OF_COLUMNS:
696
+ metrics?.dataColumns.elapsedTimeTillReceived.observe({ receivedOrder: receivedColumns }, delaySec);
697
+ break;
698
+ }
699
+ if (!blockInput.hasComputedAllData()) {
700
+ // immediately attempt fetch of data columns from execution engine
701
+ chain.getBlobsTracker.triggerGetBlobs(blockInput);
702
+ // if we've received at least half of the columns, trigger reconstruction of the rest
703
+ if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) {
704
+ chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput);
705
+ }
706
+ }
707
+ if (!blockInput.hasBlockAndAllData()) {
708
+ const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
709
+ chain.logger.debug("Received gossip data column, waiting for full data availability", {
710
+ msToWait: cutoffTimeMs,
711
+ dataColumnIndex: index,
712
+ ...blockInputMeta,
713
+ });
714
+ // do not await here to not delay gossip validation
715
+ blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => {
716
+ chain.logger.debug("Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput", {
717
+ dataColumnIndex: index,
718
+ ...blockInputMeta,
719
+ });
720
+ chain.emitter.emit(ChainEvent.incompleteBlockInput, {
721
+ blockInput,
722
+ peer: peerIdStr,
723
+ source: BlockInputSource.gossip,
724
+ });
725
+ });
726
+ }
675
727
  }
676
- }
677
- if (!blockInput.hasBlockAndAllData()) {
678
- const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
679
- chain.logger.debug("Received gossip data column, waiting for full data availability", {
680
- msToWait: cutoffTimeMs,
681
- dataColumnIndex: index,
682
- ...blockInputMeta,
683
- });
684
- // do not await here to not delay gossip validation
685
- blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => {
686
- chain.logger.debug("Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput", {
687
- dataColumnIndex: index,
688
- ...blockInputMeta,
689
- });
690
- chain.emitter.emit(ChainEvent.incompleteBlockInput, {
691
- blockInput,
692
- peer: peerIdStr,
693
- source: BlockInputSource.gossip,
694
- });
695
- });
696
- }
728
+ catch (e) {
729
+ logger.debug("Error handling gossip data column", { slot: dataColumnSlot, root: blockInput.blockRootHex, index }, e);
730
+ }
731
+ });
697
732
  }
698
733
  },
699
734
  [GossipType.beacon_aggregate_and_proof]: async ({ gossipData, topic, seenTimestampSec, }) => {
@@ -710,62 +745,76 @@ function getSequentialHandlers(modules, options) {
710
745
  }
711
746
  throw e;
712
747
  }
713
- // Handler
748
+ // Handler - deferred to next event loop so the validation result propagates first
714
749
  const { indexedAttestation, committeeValidatorIndices, attDataRootHex } = validationResult;
715
- chain.validatorMonitor?.registerGossipAggregatedAttestation(seenTimestampSec, signedAggregateAndProof, indexedAttestation);
716
- const aggregatedAttestation = signedAggregateAndProof.message.aggregate;
717
- const insertOutcome = chain.aggregatedAttestationPool.add(aggregatedAttestation, attDataRootHex, indexedAttestation.attestingIndices.length, committeeValidatorIndices);
718
- metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({ insertOutcome });
719
- if (!options.dontSendGossipAttestationsToForkchoice) {
750
+ callInNextEventLoop(() => {
720
751
  try {
721
- chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
752
+ chain.validatorMonitor?.registerGossipAggregatedAttestation(seenTimestampSec, signedAggregateAndProof, indexedAttestation);
753
+ const aggregatedAttestation = signedAggregateAndProof.message.aggregate;
754
+ const insertOutcome = chain.aggregatedAttestationPool.add(aggregatedAttestation, attDataRootHex, indexedAttestation.attestingIndices.length, committeeValidatorIndices);
755
+ metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({ insertOutcome });
756
+ if (!options.dontSendGossipAttestationsToForkchoice) {
757
+ chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
758
+ }
759
+ chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate);
722
760
  }
723
761
  catch (e) {
724
- logger.debug("Error adding gossip aggregated attestation to forkchoice", { slot: aggregatedAttestation.data.slot }, e);
762
+ logger.debug("Error handling gossip aggregate and proof", {
763
+ slot: signedAggregateAndProof.message.aggregate.data.slot,
764
+ root: toRootHex(signedAggregateAndProof.message.aggregate.data.beaconBlockRoot),
765
+ }, e);
725
766
  }
726
- }
727
- chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate);
767
+ });
728
768
  },
729
769
  [GossipType.attester_slashing]: async ({ gossipData, topic, }) => {
730
770
  const { serializedData } = gossipData;
731
771
  const { fork } = topic.boundary;
732
772
  const attesterSlashing = sszDeserialize(topic, serializedData);
733
773
  await validateGossipAttesterSlashing(chain, attesterSlashing);
734
- // Handler
735
- try {
736
- chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
737
- chain.forkChoice.onAttesterSlashing(attesterSlashing);
738
- }
739
- catch (e) {
740
- logger.error("Error adding attesterSlashing to pool", {}, e);
741
- }
742
- chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
774
+ // Handler - deferred to next event loop so the validation result propagates first
775
+ callInNextEventLoop(() => {
776
+ try {
777
+ chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
778
+ chain.forkChoice.onAttesterSlashing(attesterSlashing);
779
+ chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
780
+ }
781
+ catch (e) {
782
+ logger.debug("Error handling gossip attester slashing", { slot: attesterSlashing.attestation1.data.slot }, e);
783
+ }
784
+ });
743
785
  },
744
786
  [GossipType.proposer_slashing]: async ({ gossipData, topic, }) => {
745
787
  const { serializedData } = gossipData;
746
788
  const proposerSlashing = sszDeserialize(topic, serializedData);
747
789
  await validateGossipProposerSlashing(chain, proposerSlashing);
748
- // Handler
749
- try {
750
- chain.opPool.insertProposerSlashing(proposerSlashing);
751
- }
752
- catch (e) {
753
- logger.error("Error adding attesterSlashing to pool", {}, e);
754
- }
755
- chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
790
+ // Handler - deferred to next event loop so the validation result propagates first
791
+ callInNextEventLoop(() => {
792
+ try {
793
+ chain.opPool.insertProposerSlashing(proposerSlashing);
794
+ chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
795
+ }
796
+ catch (e) {
797
+ logger.debug("Error handling gossip proposer slashing", {
798
+ slot: proposerSlashing.signedHeader1.message.slot,
799
+ proposerIndex: proposerSlashing.signedHeader1.message.proposerIndex,
800
+ }, e);
801
+ }
802
+ });
756
803
  },
757
804
  [GossipType.voluntary_exit]: async ({ gossipData, topic }) => {
758
805
  const { serializedData } = gossipData;
759
806
  const voluntaryExit = sszDeserialize(topic, serializedData);
760
807
  await validateGossipVoluntaryExit(chain, voluntaryExit);
761
- // Handler
762
- try {
763
- chain.opPool.insertVoluntaryExit(voluntaryExit);
764
- }
765
- catch (e) {
766
- logger.error("Error adding voluntaryExit to pool", {}, e);
767
- }
768
- chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit);
808
+ // Handler - deferred to next event loop so the validation result propagates first
809
+ callInNextEventLoop(() => {
810
+ try {
811
+ chain.opPool.insertVoluntaryExit(voluntaryExit);
812
+ chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit);
813
+ }
814
+ catch (e) {
815
+ logger.debug("Error handling gossip voluntary exit", { epoch: voluntaryExit.message.epoch, validatorIndex: voluntaryExit.message.validatorIndex }, e);
816
+ }
817
+ });
769
818
  },
770
819
  [GossipType.sync_committee_contribution_and_proof]: async ({ gossipData, topic, }) => {
771
820
  const { serializedData } = gossipData;
@@ -776,16 +825,21 @@ function getSequentialHandlers(modules, options) {
776
825
  }
777
826
  throw e;
778
827
  });
779
- // Handler
780
- chain.validatorMonitor?.registerGossipSyncContributionAndProof(contributionAndProof.message, syncCommitteeParticipantIndices);
781
- try {
782
- const insertOutcome = chain.syncContributionAndProofPool.add(contributionAndProof.message, syncCommitteeParticipantIndices.length);
783
- metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({ insertOutcome });
784
- }
785
- catch (e) {
786
- logger.error("Error adding to contributionAndProof pool", {}, e);
787
- }
788
- chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof);
828
+ // Handler - deferred to next event loop so the validation result propagates first
829
+ callInNextEventLoop(() => {
830
+ try {
831
+ chain.validatorMonitor?.registerGossipSyncContributionAndProof(contributionAndProof.message, syncCommitteeParticipantIndices);
832
+ const insertOutcome = chain.syncContributionAndProofPool.add(contributionAndProof.message, syncCommitteeParticipantIndices.length);
833
+ metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({ insertOutcome });
834
+ chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof);
835
+ }
836
+ catch (e) {
837
+ logger.debug("Error handling gossip contribution and proof", {
838
+ slot: contributionAndProof.message.contribution.slot,
839
+ subcommitteeIndex: contributionAndProof.message.contribution.subcommitteeIndex,
840
+ }, e);
841
+ }
842
+ });
789
843
  },
790
844
  [GossipType.sync_committee]: async ({ gossipData, topic }) => {
791
845
  const { serializedData } = gossipData;
@@ -801,16 +855,19 @@ function getSequentialHandlers(modules, options) {
801
855
  }
802
856
  throw e;
803
857
  }
804
- // Handler — add for ALL positions this validator holds in the subcommittee
805
- try {
806
- for (const indexInSubcommittee of indicesInSubcommittee) {
807
- const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
808
- metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({ insertOutcome });
858
+ // Handler - deferred to next event loop so the validation result propagates first
859
+ // add for ALL positions this validator holds in the subcommittee
860
+ callInNextEventLoop(() => {
861
+ try {
862
+ for (const indexInSubcommittee of indicesInSubcommittee) {
863
+ const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
864
+ metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({ insertOutcome });
865
+ }
809
866
  }
810
- }
811
- catch (e) {
812
- logger.debug("Error adding to syncCommittee pool", { subnet }, e);
813
- }
867
+ catch (e) {
868
+ logger.debug("Error handling gossip sync committee", { slot: syncCommittee.slot, subnet, validatorIndex: syncCommittee.validatorIndex }, e);
869
+ }
870
+ });
814
871
  },
815
872
  [GossipType.light_client_finality_update]: async ({ gossipData, topic, }) => {
816
873
  const { serializedData } = gossipData;
@@ -827,14 +884,16 @@ function getSequentialHandlers(modules, options) {
827
884
  const { serializedData } = gossipData;
828
885
  const blsToExecutionChange = sszDeserialize(topic, serializedData);
829
886
  await validateGossipBlsToExecutionChange(chain, blsToExecutionChange);
830
- // Handler
831
- try {
832
- chain.opPool.insertBlsToExecutionChange(blsToExecutionChange);
833
- }
834
- catch (e) {
835
- logger.error("Error adding blsToExecutionChange to pool", {}, e);
836
- }
837
- chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange);
887
+ // Handler - deferred to next event loop so the validation result propagates first
888
+ callInNextEventLoop(() => {
889
+ try {
890
+ chain.opPool.insertBlsToExecutionChange(blsToExecutionChange);
891
+ chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange);
892
+ }
893
+ catch (e) {
894
+ logger.debug("Error handling gossip bls to execution change", { validatorIndex: blsToExecutionChange.message.validatorIndex }, e);
895
+ }
896
+ });
838
897
  },
839
898
  [GossipType.execution_payload]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }) => {
840
899
  const { serializedData } = gossipData;
@@ -878,103 +937,137 @@ function getSequentialHandlers(modules, options) {
878
937
  blockRoot: blockRootHex,
879
938
  });
880
939
  }
881
- chain.serializedCache.set(signedEnvelope, serializedData);
882
- payloadInput.addPayloadEnvelope({
883
- envelope: signedEnvelope,
884
- source: PayloadEnvelopeInputSource.gossip,
885
- seenTimestampSec,
886
- peerIdStr,
887
- });
888
- chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
889
- slot,
890
- builderIndex: envelope.builderIndex,
891
- blockHash: toRootHex(envelope.payload.blockHash),
892
- blockRoot: blockRootHex,
893
- });
894
- chain.processExecutionPayload(payloadInput, { validSignature: true }).catch((e) => {
895
- // Adjust verbosity based on error type
896
- let logLevel;
897
- if (e instanceof PayloadError) {
898
- switch (e.type.code) {
899
- // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
900
- // already verified the block is in fork choice
901
- case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
902
- case PayloadErrorCode.MISS_BLOCK_STATE:
903
- case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
904
- // Errors might indicate an issue with our node or the connected EL client
940
+ // Handler - deferred to next event loop so the validation result propagates first
941
+ callInNextEventLoop(() => {
942
+ try {
943
+ chain.serializedCache.set(signedEnvelope, serializedData);
944
+ payloadInput.addPayloadEnvelope({
945
+ envelope: signedEnvelope,
946
+ source: PayloadEnvelopeInputSource.gossip,
947
+ seenTimestampSec,
948
+ peerIdStr,
949
+ });
950
+ chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
951
+ slot,
952
+ builderIndex: envelope.builderIndex,
953
+ blockHash: toRootHex(envelope.payload.blockHash),
954
+ blockRoot: blockRootHex,
955
+ });
956
+ chain.processExecutionPayload(payloadInput, { validSignature: true }).catch((e) => {
957
+ // Adjust verbosity based on error type
958
+ let logLevel;
959
+ if (e instanceof PayloadError) {
960
+ switch (e.type.code) {
961
+ // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
962
+ // already verified the block is in fork choice
963
+ case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
964
+ case PayloadErrorCode.MISS_BLOCK_STATE:
965
+ case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
966
+ // Errors might indicate an issue with our node or the connected EL client
967
+ logLevel = LogLevel.error;
968
+ break;
969
+ // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
970
+ case PayloadErrorCode.INVALID_SIGNATURE:
971
+ case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
972
+ case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
973
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
974
+ // Misbehaving peer, but could highlight an issue in another client
975
+ logLevel = LogLevel.warn;
976
+ break;
977
+ }
978
+ }
979
+ else {
980
+ // Any unexpected error
905
981
  logLevel = LogLevel.error;
906
- break;
907
- // INVALID_SIGNATURE should not happen, signature is verified during gossip validation
908
- case PayloadErrorCode.INVALID_SIGNATURE:
909
- case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
910
- case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
911
- core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
912
- // Misbehaving peer, but could highlight an issue in another client
913
- logLevel = LogLevel.warn;
914
- break;
915
- }
982
+ }
983
+ metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
984
+ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
985
+ });
986
+ chain.logger[logLevel]("Error processing execution payload from gossip", { slot, root: blockRootHex, peer: peerIdStr }, e);
987
+ });
916
988
  }
917
- else {
918
- // Any unexpected error
919
- logLevel = LogLevel.error;
989
+ catch (e) {
990
+ logger.debug("Error handling gossip execution payload", { slot, root: blockRootHex }, e);
920
991
  }
921
- metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
922
- error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
923
- });
924
- chain.logger[logLevel]("Error processing execution payload from gossip", { slot, peer: peerIdStr, root: blockRootHex }, e);
925
992
  });
926
993
  },
927
994
  [GossipType.payload_attestation_message]: async ({ gossipData, topic, seenTimestampSec, }) => {
928
995
  const { serializedData } = gossipData;
929
996
  const payloadAttestationMessage = sszDeserialize(topic, serializedData);
930
997
  const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage);
931
- const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
932
- metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({ source: OpSource.gossip }, delaySec);
933
- const nextSlotProposer = chain.getHeadState().getBeaconProposer(payloadAttestationMessage.data.slot + 1);
934
- const isNextSlotProposer = chain.beaconProposerCache.get(nextSlotProposer) !== undefined;
935
- if (isNextSlotProposer) {
998
+ // Handler - deferred to next event loop so the validation result propagates first
999
+ callInNextEventLoop(() => {
936
1000
  try {
937
- const insertOutcome = chain.payloadAttestationPool.add(payloadAttestationMessage, validationResult.attDataRootHex, validationResult.validatorCommitteeIndices);
938
- metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({ insertOutcome });
1001
+ const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
1002
+ metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({ source: OpSource.gossip }, delaySec);
1003
+ const nextSlotProposer = chain.getHeadState().getBeaconProposer(payloadAttestationMessage.data.slot + 1);
1004
+ const isNextSlotProposer = chain.beaconProposerCache.get(nextSlotProposer) !== undefined;
1005
+ if (isNextSlotProposer) {
1006
+ try {
1007
+ const insertOutcome = chain.payloadAttestationPool.add(payloadAttestationMessage, validationResult.attDataRootHex, validationResult.validatorCommitteeIndices);
1008
+ metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({ insertOutcome });
1009
+ }
1010
+ catch (e) {
1011
+ logger.debug("Error adding to payloadAttestation pool", {}, e);
1012
+ }
1013
+ }
1014
+ chain.forkChoice.notifyPtcMessages(toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validationResult.validatorCommitteeIndices, payloadAttestationMessage.data.payloadPresent, payloadAttestationMessage.data.blobDataAvailable);
1015
+ chain.emitter.emit(routes.events.EventType.payloadAttestationMessage, {
1016
+ version: config.getForkName(payloadAttestationMessage.data.slot),
1017
+ data: payloadAttestationMessage,
1018
+ });
939
1019
  }
940
1020
  catch (e) {
941
- logger.debug("Error adding to payloadAttestation pool", {}, e);
1021
+ logger.debug("Error handling gossip payload attestation message", {
1022
+ slot: payloadAttestationMessage.data.slot,
1023
+ root: toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1024
+ }, e);
942
1025
  }
943
- }
944
- chain.forkChoice.notifyPtcMessages(toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validationResult.validatorCommitteeIndices, payloadAttestationMessage.data.payloadPresent, payloadAttestationMessage.data.blobDataAvailable);
945
- chain.emitter.emit(routes.events.EventType.payloadAttestationMessage, {
946
- version: config.getForkName(payloadAttestationMessage.data.slot),
947
- data: payloadAttestationMessage,
948
1026
  });
949
1027
  },
950
1028
  [GossipType.execution_payload_bid]: async ({ gossipData, topic, seenTimestampSec, }) => {
951
1029
  const { serializedData } = gossipData;
952
1030
  const executionPayloadBid = sszDeserialize(topic, serializedData);
953
1031
  const { proposerIndex } = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
954
- // this could be negative, because it's most likely the bid of next slot comes at this clock slot
955
- const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
956
- metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({ source: OpSource.gossip }, elapsedSec);
957
- // Handle valid payload bid by storing in a bid pool
958
- try {
959
- const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid, Math.floor(elapsedSec * 1000));
960
- metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({ insertOutcome });
961
- }
962
- catch (e) {
963
- logger.error("Error adding to executionPayloadBid pool", {}, e);
964
- }
965
- chain.validatorMonitor?.registerExecutionPayloadBid(OpSource.gossip, proposerIndex, executionPayloadBid.message);
966
- chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
967
- version: config.getForkName(executionPayloadBid.message.slot),
968
- data: executionPayloadBid,
1032
+ // Handler - deferred to next event loop so the validation result propagates first
1033
+ callInNextEventLoop(() => {
1034
+ try {
1035
+ // this could be negative, because it's most likely the bid of next slot comes at this clock slot
1036
+ const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
1037
+ metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({ source: OpSource.gossip }, elapsedSec);
1038
+ // Handle valid payload bid by storing in a bid pool
1039
+ const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid, Math.floor(elapsedSec * 1000));
1040
+ metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({ insertOutcome });
1041
+ chain.validatorMonitor?.registerExecutionPayloadBid(OpSource.gossip, proposerIndex, executionPayloadBid.message);
1042
+ chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
1043
+ version: config.getForkName(executionPayloadBid.message.slot),
1044
+ data: executionPayloadBid,
1045
+ });
1046
+ }
1047
+ catch (e) {
1048
+ logger.debug("Error handling gossip execution payload bid", {
1049
+ slot: executionPayloadBid.message.slot,
1050
+ root: toRootHex(executionPayloadBid.message.parentBlockRoot),
1051
+ proposerIndex,
1052
+ }, e);
1053
+ }
969
1054
  });
970
1055
  },
971
1056
  [GossipType.proposer_preferences]: async ({ gossipData, topic, }) => {
972
1057
  const { serializedData } = gossipData;
973
1058
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
974
1059
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
975
- chain.emitter.emit(routes.events.EventType.proposerPreferences, {
976
- version: config.getForkName(signedProposerPreferences.message.proposalSlot),
977
- data: signedProposerPreferences,
1060
+ // Handler - deferred to next event loop so the validation result propagates first
1061
+ callInNextEventLoop(() => {
1062
+ try {
1063
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1064
+ version: config.getForkName(signedProposerPreferences.message.proposalSlot),
1065
+ data: signedProposerPreferences,
1066
+ });
1067
+ }
1068
+ catch (e) {
1069
+ logger.debug("Error handling gossip proposer preferences", { proposalSlot: signedProposerPreferences.message.proposalSlot }, e);
1070
+ }
978
1071
  });
979
1072
  },
980
1073
  };
@@ -1001,43 +1094,9 @@ function getBatchHandlers(modules, options) {
1001
1094
  subnet: param.topic.subnet,
1002
1095
  }));
1003
1096
  const { results: validationResults, batchableBls } = await validateGossipAttestationsSameAttData(fork, chain, validationParams);
1004
- for (const [i, validationResult] of validationResults.entries()) {
1005
- if (validationResult.err) {
1006
- results.push(validationResult.err);
1007
- continue;
1008
- }
1009
- // null means no error
1010
- results.push(null);
1011
- // Handler
1012
- const { indexedAttestation, attDataRootHex, attestation, committeeIndex, validatorCommitteeIndex, committeeSize, } = validationResult.result;
1013
- chain.validatorMonitor?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation);
1014
- const { subnet } = validationResult.result;
1015
- try {
1016
- // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages
1017
- // but don't add to attestation pool, to save CPU and RAM
1018
- if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) {
1019
- const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex, validatorCommitteeIndex, committeeSize);
1020
- metrics?.opPool.attestationPool.gossipInsertOutcome.inc({ insertOutcome });
1021
- }
1022
- }
1023
- catch (e) {
1024
- logger.error("Error adding unaggregated attestation to pool", { subnet }, e);
1025
- }
1026
- if (!options.dontSendGossipAttestationsToForkchoice) {
1027
- try {
1028
- chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1029
- }
1030
- catch (e) {
1031
- logger.debug("Error adding gossip unaggregated attestation to forkchoice", { subnet }, e);
1032
- }
1033
- }
1034
- if (isForkPostElectra(fork)) {
1035
- chain.emitter.emit(routes.events.EventType.singleAttestation, attestation);
1036
- }
1037
- else {
1038
- chain.emitter.emit(routes.events.EventType.attestation, attestation);
1039
- chain.emitter.emit(routes.events.EventType.singleAttestation, toElectraSingleAttestation(attestation, indexedAttestation.attestingIndices[0]));
1040
- }
1097
+ // verdicts computed synchronously and returned immediately
1098
+ for (const validationResult of validationResults) {
1099
+ results.push(validationResult.err ? validationResult.err : null);
1041
1100
  }
1042
1101
  if (batchableBls) {
1043
1102
  metrics?.gossipAttestation.attestationBatchHistogram.observe(attestationCount);
@@ -1045,6 +1104,47 @@ function getBatchHandlers(modules, options) {
1045
1104
  else {
1046
1105
  metrics?.gossipAttestation.attestationNonBatchCount.inc(attestationCount);
1047
1106
  }
1107
+ // Handler - deferred to next event loop so the validation result propagates first.
1108
+ callInNextEventLoop(() => {
1109
+ for (const [i, validationResult] of validationResults.entries()) {
1110
+ if (validationResult.err)
1111
+ continue;
1112
+ const { indexedAttestation, attDataRootHex, attestation, committeeIndex, validatorCommitteeIndex, committeeSize, subnet, } = validationResult.result;
1113
+ try {
1114
+ chain.validatorMonitor?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation);
1115
+ try {
1116
+ // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages
1117
+ // but don't add to attestation pool, to save CPU and RAM
1118
+ if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) {
1119
+ const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex, validatorCommitteeIndex, committeeSize);
1120
+ metrics?.opPool.attestationPool.gossipInsertOutcome.inc({ insertOutcome });
1121
+ }
1122
+ }
1123
+ catch (e) {
1124
+ logger.debug("Error adding gossip unaggregated attestation to pool", { subnet }, e);
1125
+ }
1126
+ // Separate boundary: a pool insertion error above must not skip the fork-choice vote
1127
+ if (!options.dontSendGossipAttestationsToForkchoice) {
1128
+ try {
1129
+ chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex);
1130
+ }
1131
+ catch (e) {
1132
+ logger.debug("Error adding gossip unaggregated attestation to forkchoice", { subnet }, e);
1133
+ }
1134
+ }
1135
+ if (isForkPostElectra(fork)) {
1136
+ chain.emitter.emit(routes.events.EventType.singleAttestation, attestation);
1137
+ }
1138
+ else {
1139
+ chain.emitter.emit(routes.events.EventType.attestation, attestation);
1140
+ chain.emitter.emit(routes.events.EventType.singleAttestation, toElectraSingleAttestation(attestation, indexedAttestation.attestingIndices[0]));
1141
+ }
1142
+ }
1143
+ catch (e) {
1144
+ logger.debug("Error handling gossip attestation", { slot: indexedAttestation.data.slot, subnet }, e);
1145
+ }
1146
+ }
1147
+ });
1048
1148
  return results;
1049
1149
  },
1050
1150
  };