@lodestar/beacon-node 1.51.0-dev.aa1ecb7c18 → 1.51.0-dev.ab8f3f6381

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 (167) hide show
  1. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/pool/index.js +17 -0
  3. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  4. package/lib/chain/GetBlobsTracker.d.ts +2 -2
  5. package/lib/chain/GetBlobsTracker.d.ts.map +1 -1
  6. package/lib/chain/GetBlobsTracker.js.map +1 -1
  7. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  8. package/lib/chain/blocks/importBlock.js +35 -47
  9. package/lib/chain/blocks/importBlock.js.map +1 -1
  10. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  11. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  12. package/lib/chain/blocks/importExecutionPayload.js +24 -3
  13. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  14. package/lib/chain/blocks/index.d.ts.map +1 -1
  15. package/lib/chain/blocks/index.js +2 -2
  16. package/lib/chain/blocks/index.js.map +1 -1
  17. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +1 -1
  18. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  19. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  20. package/lib/chain/blocks/verifyBlocksSanityChecks.js +4 -0
  21. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  22. package/lib/chain/chain.d.ts +24 -2
  23. package/lib/chain/chain.d.ts.map +1 -1
  24. package/lib/chain/chain.js +119 -6
  25. package/lib/chain/chain.js.map +1 -1
  26. package/lib/chain/emitter.d.ts +6 -0
  27. package/lib/chain/emitter.d.ts.map +1 -1
  28. package/lib/chain/emitter.js +5 -0
  29. package/lib/chain/emitter.js.map +1 -1
  30. package/lib/chain/forkChoice/index.d.ts +11 -1
  31. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  32. package/lib/chain/forkChoice/index.js +11 -0
  33. package/lib/chain/forkChoice/index.js.map +1 -1
  34. package/lib/chain/forkChoiceExecutionEngine.d.ts +53 -0
  35. package/lib/chain/forkChoiceExecutionEngine.d.ts.map +1 -0
  36. package/lib/chain/forkChoiceExecutionEngine.js +121 -0
  37. package/lib/chain/forkChoiceExecutionEngine.js.map +1 -0
  38. package/lib/chain/interface.d.ts +7 -3
  39. package/lib/chain/interface.d.ts.map +1 -1
  40. package/lib/chain/interface.js.map +1 -1
  41. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +17 -4
  42. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  43. package/lib/chain/opPools/aggregatedAttestationPool.js +177 -37
  44. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  45. package/lib/chain/opPools/index.d.ts +1 -0
  46. package/lib/chain/opPools/index.d.ts.map +1 -1
  47. package/lib/chain/opPools/index.js +1 -0
  48. package/lib/chain/opPools/index.js.map +1 -1
  49. package/lib/chain/opPools/singleAttestationPool.d.ts +77 -0
  50. package/lib/chain/opPools/singleAttestationPool.d.ts.map +1 -0
  51. package/lib/chain/opPools/singleAttestationPool.js +209 -0
  52. package/lib/chain/opPools/singleAttestationPool.js.map +1 -0
  53. package/lib/chain/options.d.ts +5 -0
  54. package/lib/chain/options.d.ts.map +1 -1
  55. package/lib/chain/options.js.map +1 -1
  56. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -2
  57. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  58. package/lib/chain/produceBlock/produceBlockBody.js +3 -2
  59. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  60. package/lib/chain/proposerTracker.d.ts +31 -0
  61. package/lib/chain/proposerTracker.d.ts.map +1 -0
  62. package/lib/chain/proposerTracker.js +75 -0
  63. package/lib/chain/proposerTracker.js.map +1 -0
  64. package/lib/chain/regen/interface.d.ts +1 -1
  65. package/lib/chain/regen/interface.d.ts.map +1 -1
  66. package/lib/chain/regen/queued.d.ts +1 -1
  67. package/lib/chain/regen/queued.d.ts.map +1 -1
  68. package/lib/chain/regen/queued.js +5 -3
  69. package/lib/chain/regen/queued.js.map +1 -1
  70. package/lib/execution/engine/http.d.ts +2 -2
  71. package/lib/execution/engine/http.d.ts.map +1 -1
  72. package/lib/execution/engine/http.js +5 -4
  73. package/lib/execution/engine/http.js.map +1 -1
  74. package/lib/execution/engine/interface.d.ts +16 -2
  75. package/lib/execution/engine/interface.d.ts.map +1 -1
  76. package/lib/execution/engine/jsonRpcTransport.d.ts +2 -2
  77. package/lib/execution/engine/jsonRpcTransport.d.ts.map +1 -1
  78. package/lib/execution/engine/jsonRpcTransport.js +1 -1
  79. package/lib/execution/engine/jsonRpcTransport.js.map +1 -1
  80. package/lib/execution/engine/restTransport.d.ts +2 -2
  81. package/lib/execution/engine/restTransport.d.ts.map +1 -1
  82. package/lib/execution/engine/restTransport.js.map +1 -1
  83. package/lib/execution/engine/transport.d.ts +1 -12
  84. package/lib/execution/engine/transport.d.ts.map +1 -1
  85. package/lib/index.d.ts +1 -1
  86. package/lib/index.d.ts.map +1 -1
  87. package/lib/index.js +1 -1
  88. package/lib/index.js.map +1 -1
  89. package/lib/metrics/metrics/lodestar.d.ts +26 -0
  90. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  91. package/lib/metrics/metrics/lodestar.js +69 -0
  92. package/lib/metrics/metrics/lodestar.js.map +1 -1
  93. package/lib/network/core/networkCore.d.ts +2 -1
  94. package/lib/network/core/networkCore.d.ts.map +1 -1
  95. package/lib/network/core/networkCore.js +4 -0
  96. package/lib/network/core/networkCore.js.map +1 -1
  97. package/lib/network/core/networkCoreWorker.js +1 -0
  98. package/lib/network/core/networkCoreWorker.js.map +1 -1
  99. package/lib/network/core/networkCoreWorkerHandler.d.ts +2 -1
  100. package/lib/network/core/networkCoreWorkerHandler.d.ts.map +1 -1
  101. package/lib/network/core/networkCoreWorkerHandler.js +3 -0
  102. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  103. package/lib/network/core/types.d.ts +4 -1
  104. package/lib/network/core/types.d.ts.map +1 -1
  105. package/lib/network/network.d.ts +2 -0
  106. package/lib/network/network.d.ts.map +1 -1
  107. package/lib/network/network.js +8 -0
  108. package/lib/network/network.js.map +1 -1
  109. package/lib/network/options.d.ts.map +1 -1
  110. package/lib/network/options.js +2 -0
  111. package/lib/network/options.js.map +1 -1
  112. package/lib/network/peers/utils/subnetMap.d.ts +5 -2
  113. package/lib/network/peers/utils/subnetMap.d.ts.map +1 -1
  114. package/lib/network/peers/utils/subnetMap.js +6 -3
  115. package/lib/network/peers/utils/subnetMap.js.map +1 -1
  116. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  117. package/lib/network/processor/gossipHandlers.js +20 -0
  118. package/lib/network/processor/gossipHandlers.js.map +1 -1
  119. package/lib/network/subnets/attnetsService.d.ts +35 -5
  120. package/lib/network/subnets/attnetsService.d.ts.map +1 -1
  121. package/lib/network/subnets/attnetsService.js +84 -6
  122. package/lib/network/subnets/attnetsService.js.map +1 -1
  123. package/lib/network/subnets/interface.d.ts +7 -0
  124. package/lib/network/subnets/interface.d.ts.map +1 -1
  125. package/lib/network/subnets/interface.js.map +1 -1
  126. package/lib/util/execution.d.ts +4 -4
  127. package/lib/util/execution.d.ts.map +1 -1
  128. package/lib/util/execution.js.map +1 -1
  129. package/package.json +16 -16
  130. package/src/api/impl/beacon/pool/index.ts +26 -0
  131. package/src/chain/GetBlobsTracker.ts +2 -2
  132. package/src/chain/blocks/importBlock.ts +55 -67
  133. package/src/chain/blocks/importExecutionPayload.ts +42 -13
  134. package/src/chain/blocks/index.ts +5 -2
  135. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +1 -1
  136. package/src/chain/blocks/verifyBlocksSanityChecks.ts +4 -1
  137. package/src/chain/chain.ts +134 -3
  138. package/src/chain/emitter.ts +7 -0
  139. package/src/chain/forkChoice/index.ts +11 -0
  140. package/src/chain/forkChoiceExecutionEngine.ts +197 -0
  141. package/src/chain/interface.ts +7 -2
  142. package/src/chain/opPools/aggregatedAttestationPool.ts +222 -35
  143. package/src/chain/opPools/index.ts +1 -0
  144. package/src/chain/opPools/singleAttestationPool.ts +287 -0
  145. package/src/chain/options.ts +5 -0
  146. package/src/chain/produceBlock/produceBlockBody.ts +7 -3
  147. package/src/chain/proposerTracker.ts +94 -0
  148. package/src/chain/regen/interface.ts +1 -1
  149. package/src/chain/regen/queued.ts +6 -4
  150. package/src/execution/engine/http.ts +12 -13
  151. package/src/execution/engine/interface.ts +18 -2
  152. package/src/execution/engine/jsonRpcTransport.ts +10 -2
  153. package/src/execution/engine/restTransport.ts +3 -1
  154. package/src/execution/engine/transport.ts +8 -14
  155. package/src/index.ts +1 -1
  156. package/src/metrics/metrics/lodestar.ts +70 -0
  157. package/src/network/core/networkCore.ts +6 -1
  158. package/src/network/core/networkCoreWorker.ts +1 -0
  159. package/src/network/core/networkCoreWorkerHandler.ts +4 -1
  160. package/src/network/core/types.ts +4 -1
  161. package/src/network/network.ts +10 -0
  162. package/src/network/options.ts +2 -0
  163. package/src/network/peers/utils/subnetMap.ts +6 -3
  164. package/src/network/processor/gossipHandlers.ts +29 -0
  165. package/src/network/subnets/attnetsService.ts +94 -6
  166. package/src/network/subnets/interface.ts +7 -0
  167. package/src/util/execution.ts +6 -6
@@ -1347,8 +1347,67 @@ export function createLodestarMetrics(
1347
1347
  name: "lodestar_oppool_aggregated_attestation_pool_packed_attestations_missing_shuffling_committees_total",
1348
1348
  help: "Total number of committees skipped due to missing shuffling when producing packed attestations",
1349
1349
  }),
1350
+ attestationValue: register.counter<{source: "aggregate" | "single"}>({
1351
+ name: "lodestar_oppool_packed_attestation_value_total",
1352
+ help: "Sum of new seen effective balance increments times participation flag weight of packed attestations, by source",
1353
+ labelNames: ["source"],
1354
+ }),
1355
+ blockNewSeenAttesters: register.gauge({
1356
+ name: "lodestar_oppool_packed_attestations_block_new_seen_attesters",
1357
+ help: "Total number of new seen attesters across all packed attestations of the last produced block",
1358
+ }),
1359
+ singlesValueRatio: register.histogram({
1360
+ name: "lodestar_oppool_packed_attestations_singles_value_ratio",
1361
+ help: "Ratio of attestation value contributed by single attestations per produced block",
1362
+ buckets: [0, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1],
1363
+ }),
1364
+ singlesNewSeenAttesters: register.histogram({
1365
+ name: "lodestar_oppool_packed_attestations_singles_new_seen_attesters",
1366
+ help: "Number of attesters newly included only thanks to single attestations per produced block",
1367
+ buckets: [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000, 1024],
1368
+ }),
1369
+ singlesNewSeenAttestersByInclusionDistance: register.gauge<{inclusionDistance: number}>({
1370
+ name: "lodestar_oppool_packed_attestations_singles_new_seen_attesters_by_inclusion_distance",
1371
+ help: "Number of attesters newly included only thanks to single attestations in the last produced block, by inclusion distance",
1372
+ labelNames: ["inclusionDistance"],
1373
+ }),
1374
+ singlesNewSeenEffectiveBalance: register.histogram({
1375
+ name: "lodestar_oppool_packed_attestations_singles_new_seen_effective_balance",
1376
+ help: "Effective balance increments of attesters newly included only thanks to single attestations per produced block",
1377
+ buckets: [0, 32, 160, 320, 800, 1600, 3200, 8000, 32000],
1378
+ }),
1379
+ singlesOnlyConsolidations: register.histogram({
1380
+ name: "lodestar_oppool_packed_attestations_singles_only_consolidations",
1381
+ help: "Number of packed attestations built only from single attestations per produced block",
1382
+ buckets: [0, 1, 2, 4, 8],
1383
+ }),
1384
+ singlesFoldedCommittees: register.histogram({
1385
+ name: "lodestar_oppool_packed_attestations_singles_folded_committees",
1386
+ help: "Number of committees in packed attestations that received single attestations per produced block",
1387
+ buckets: [0, 1, 2, 4, 8, 16, 32, 64],
1388
+ }),
1350
1389
  },
1351
1390
  },
1391
+ singleAttestationPool: {
1392
+ size: register.gauge({
1393
+ name: "lodestar_oppool_single_attestation_pool_size",
1394
+ help: "Current number of single attestations in the SingleAttestationPool",
1395
+ }),
1396
+ gossipInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
1397
+ name: "lodestar_oppool_single_attestation_pool_gossip_insert_outcome_total",
1398
+ help: "Total number of InsertOutcome as a result of adding a single attestation from gossip to the SingleAttestationPool",
1399
+ labelNames: ["insertOutcome"],
1400
+ }),
1401
+ apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
1402
+ name: "lodestar_oppool_single_attestation_pool_api_insert_outcome_total",
1403
+ help: "Total number of InsertOutcome as a result of adding a single attestation from api to the SingleAttestationPool",
1404
+ labelNames: ["insertOutcome"],
1405
+ }),
1406
+ deletedByBlock: register.counter({
1407
+ name: "lodestar_oppool_single_attestation_pool_deleted_by_block_total",
1408
+ help: "Total number of single attestations deleted because a canonical block includes them",
1409
+ }),
1410
+ },
1352
1411
  attestationPool: {
1353
1412
  size: register.gauge({
1354
1413
  name: "lodestar_oppool_attestation_pool_size",
@@ -1497,6 +1556,17 @@ export function createLodestarMetrics(
1497
1556
  },
1498
1557
  },
1499
1558
 
1559
+ proposerTracker: {
1560
+ proposalSlots: register.gauge({
1561
+ name: "lodestar_proposer_tracker_proposal_slots",
1562
+ help: "Current number of upcoming block proposals of connected validators tracked by the ProposerTracker",
1563
+ }),
1564
+ collectableSlot: register.gauge({
1565
+ name: "lodestar_proposer_tracker_collectable_slot",
1566
+ help: "1 if single attestations of the clock slot are collected for an upcoming local proposal, 0 otherwise",
1567
+ }),
1568
+ },
1569
+
1500
1570
  chain: {
1501
1571
  blacklistedBlocks: register.gauge({
1502
1572
  name: "lodestar_blacklisted_blocks_total",
@@ -8,7 +8,7 @@ import {BeaconConfig, ForkBoundary} from "@lodestar/config";
8
8
  import type {LoggerNode} from "@lodestar/logger/node";
9
9
  import {isForkPostFulu} from "@lodestar/params";
10
10
  import {ResponseIncoming} from "@lodestar/reqresp";
11
- import {Epoch, Status, fulu, sszTypesFor} from "@lodestar/types";
11
+ import {Epoch, Slot, Status, fulu, sszTypesFor} from "@lodestar/types";
12
12
  import {formatNodePeer} from "../../api/impl/node/utils.js";
13
13
  import {GoodByeReasonCode} from "../../constants/index.js";
14
14
  import {RegistryMetricCreator} from "../../metrics/index.js";
@@ -345,6 +345,11 @@ export class NetworkCore implements INetworkCore {
345
345
  if (subscriptions.length > 0) this.peerManager.onCommitteeSubscriptions();
346
346
  }
347
347
 
348
+ async addLocalProposalSlots(slots: Slot[]): Promise<void> {
349
+ // no peerManager.onCommitteeSubscriptions(): the extra subscription relies on connected peers, no discovery
350
+ this.attnetsService.addLocalProposalSlots(slots);
351
+ }
352
+
348
353
  async prepareSyncCommitteeSubnets(subscriptions: CommitteeSubscription[]): Promise<void> {
349
354
  this.syncnetsService.addCommitteeSubscriptions(subscriptions);
350
355
  if (subscriptions.length > 0) this.peerManager.onCommitteeSubscriptions();
@@ -136,6 +136,7 @@ const libp2pWorkerApi: NetworkWorkerApi = {
136
136
 
137
137
  prepareBeaconCommitteeSubnets: (subscriptions) => core.prepareBeaconCommitteeSubnets(subscriptions),
138
138
  prepareSyncCommitteeSubnets: (subscriptions) => core.prepareSyncCommitteeSubnets(subscriptions),
139
+ addLocalProposalSlots: (slots) => core.addLocalProposalSlots(slots),
139
140
  reportPeer: (peer, action, actionName) => core.reportPeer(peer, action, actionName),
140
141
  reStatusPeers: (peers) => core.reStatusPeers(peers),
141
142
  goodbyeAndDisconnect: (peer, reason) => core.goodbyeAndDisconnect(peer, reason),
@@ -9,7 +9,7 @@ import {routes} from "@lodestar/api";
9
9
  import {BeaconConfig, chainConfigToJson} from "@lodestar/config";
10
10
  import type {LoggerNode} from "@lodestar/logger/node";
11
11
  import {ResponseIncoming, ResponseOutgoing} from "@lodestar/reqresp";
12
- import {Status} from "@lodestar/types";
12
+ import {Slot, Status} from "@lodestar/types";
13
13
  import {withTimeout} from "@lodestar/utils";
14
14
  import {GoodByeReasonCode} from "../../constants/index.js";
15
15
  import {Metrics} from "../../metrics/index.js";
@@ -220,6 +220,9 @@ export class WorkerNetworkCore implements INetworkCore {
220
220
  prepareSyncCommitteeSubnets(subscriptions: CommitteeSubscription[]): Promise<void> {
221
221
  return this.getApi().prepareSyncCommitteeSubnets(subscriptions);
222
222
  }
223
+ addLocalProposalSlots(slots: Slot[]): Promise<void> {
224
+ return this.getApi().addLocalProposalSlots(slots);
225
+ }
223
226
  subscribeGossipCoreTopics(): Promise<void> {
224
227
  return this.getApi().subscribeGossipCoreTopics();
225
228
  }
@@ -4,7 +4,7 @@ import {routes} from "@lodestar/api";
4
4
  import {SpecJson} from "@lodestar/config";
5
5
  import {LoggerNodeOpts} from "@lodestar/logger/node";
6
6
  import {ResponseIncoming} from "@lodestar/reqresp";
7
- import {Status} from "@lodestar/types";
7
+ import {Slot, Status} from "@lodestar/types";
8
8
  import {GoodByeReasonCode} from "../../constants/index.js";
9
9
  import {PeerIdStr} from "../../util/peerId.js";
10
10
  import {NetworkOptions} from "../options.js";
@@ -62,6 +62,8 @@ export interface INetworkCore extends INetworkCorePublic {
62
62
 
63
63
  /** Chain must push status updates to the network core */
64
64
  updateStatus(status: Status): Promise<void>;
65
+ /** Block proposal slots of connected validators, used to subscribe to all attestation subnets before them */
66
+ addLocalProposalSlots(slots: Slot[]): Promise<void>;
65
67
 
66
68
  setTargetGroupCount(count: number): Promise<void>;
67
69
  /** Opens stream to handle ReqResp outgoing request */
@@ -111,6 +113,7 @@ export type NetworkWorkerApi = INetworkCorePublic & {
111
113
  getConnectedPeers(): Promise<PeerIdStr[]>;
112
114
  getConnectedPeerCount(): Promise<number>;
113
115
  updateStatus(status: Status): Promise<void>;
116
+ addLocalProposalSlots(slots: Slot[]): Promise<void>;
114
117
 
115
118
  setTargetGroupCount(count: number): Promise<void>;
116
119
 
@@ -19,6 +19,7 @@ import {
19
19
  SignedAggregateAndProof,
20
20
  SignedBeaconBlock,
21
21
  SingleAttestation,
22
+ Slot,
22
23
  SlotRootHex,
23
24
  SubnetID,
24
25
  altair,
@@ -153,6 +154,7 @@ export class Network implements INetwork {
153
154
  this.chain.emitter.on(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars);
154
155
  this.chain.emitter.on(ChainEvent.publishProposerSlashing, this.onPublishProposerSlashing);
155
156
  this.chain.emitter.on(ChainEvent.updateStatus, this.onUpdateStatus);
157
+ this.chain.emitter.on(ChainEvent.localProposalSlots, this.onLocalProposalSlots);
156
158
  }
157
159
 
158
160
  static async init({
@@ -258,6 +260,7 @@ export class Network implements INetwork {
258
260
  this.chain.emitter.off(ChainEvent.publishBlobSidecars, this.onPublishBlobSidecars);
259
261
  this.chain.emitter.off(ChainEvent.publishProposerSlashing, this.onPublishProposerSlashing);
260
262
  this.chain.emitter.off(ChainEvent.updateStatus, this.onUpdateStatus);
263
+ this.chain.emitter.off(ChainEvent.localProposalSlots, this.onLocalProposalSlots);
261
264
  this.connectedPeersSyncMeta.clear();
262
265
  this.subscribedToCoreTopics = false;
263
266
  const results = await Promise.allSettled([this.networkProcessor.stop(), this.core.close()]);
@@ -285,6 +288,13 @@ export class Network implements INetwork {
285
288
  return this.core.prepareBeaconCommitteeSubnets(subscriptions);
286
289
  }
287
290
 
291
+ /** The core subscribes to all attestation subnets before these proposals, see `slotsToSubscribeBeforeProposalDuty` */
292
+ private onLocalProposalSlots = (proposalSlots: Slot[]): void => {
293
+ this.core.addLocalProposalSlots(proposalSlots).catch((e) => {
294
+ this.logger.debug("Error adding local proposal slots", {slots: proposalSlots.join(",")}, e as Error);
295
+ });
296
+ };
297
+
288
298
  async prepareSyncCommitteeSubnets(subscriptions: CommitteeSubscription[]): Promise<void> {
289
299
  return this.core.prepareSyncCommitteeSubnets(subscriptions);
290
300
  }
@@ -74,6 +74,8 @@ export const defaultNetworkOptions: NetworkOptions = {
74
74
  maxYoungGenerationSizeMb: 152,
75
75
  // subscribe 2 slots before aggregator dutied slot to get stable mesh peers as monitored on goerli
76
76
  slotsToSubscribeBeforeAggregatorDuty: 2,
77
+ // up to the max number of attestations per block
78
+ slotsToSubscribeBeforeProposalDuty: 8,
77
79
  // This will enable the light client server by default
78
80
  disableLightClientServer: false,
79
81
  quic: true,
@@ -66,11 +66,14 @@ export class SubnetMap {
66
66
  return subnets;
67
67
  }
68
68
 
69
- /** Return subnetIds with a `toSlot` less than `currentSlot`. Also deletes expired entries */
70
- getExpired(currentSlot: Slot): SubnetID[] {
69
+ /**
70
+ * Return subnetIds with a `toSlot` less than `currentSlot`. Also deletes expired entries
71
+ * @param keep expired subnets for which it returns true are neither returned nor deleted
72
+ */
73
+ getExpired(currentSlot: Slot, keep?: (subnet: SubnetID) => boolean): SubnetID[] {
71
74
  const subnetIds: SubnetID[] = [];
72
75
  for (const [subnet, toSlot] of this.subnets.entries()) {
73
- if (toSlot < currentSlot) {
76
+ if (toSlot < currentSlot && keep?.(subnet) !== true) {
74
77
  subnetIds.push(subnet);
75
78
  this.subnets.delete(subnet);
76
79
  }
@@ -1377,6 +1377,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1377
1377
  // Errors might indicate an issue with our node or the connected EL client
1378
1378
  logLevel = LogLevel.error;
1379
1379
  break;
1380
+ case PayloadErrorCode.BLOCK_EXECUTION_INVALID:
1381
+ logLevel = LogLevel.warn;
1382
+ break;
1380
1383
  // INVALID_SIGNATURE only happens for an envelope added by another source
1381
1384
  case PayloadErrorCode.INVALID_SIGNATURE:
1382
1385
  case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
@@ -1659,6 +1662,32 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
1659
1662
  logger.debug("Error adding gossip unaggregated attestation to pool", {subnet}, e as Error);
1660
1663
  }
1661
1664
 
1665
+ // Collected only for vote slots that an upcoming block of a connected validator can include
1666
+ if (isForkPostElectra(fork) && chain.proposerTracker.shouldCollect(indexedAttestation.data.slot)) {
1667
+ try {
1668
+ const insertOutcome = chain.singleAttestationPool.add(
1669
+ attestation as SingleAttestation<ForkPostElectra>,
1670
+ attDataRootHex,
1671
+ validatorCommitteeIndex,
1672
+ committeeSize
1673
+ );
1674
+ metrics?.opPool.singleAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1675
+ } catch (e) {
1676
+ logger.debug(
1677
+ "Error adding gossip single attestation to pool",
1678
+ {
1679
+ slot: indexedAttestation.data.slot,
1680
+ committeeIndex,
1681
+ validatorCommitteeIndex,
1682
+ validatorIndex: (attestation as SingleAttestation<ForkPostElectra>).attesterIndex,
1683
+ subnet,
1684
+ attDataRoot: attDataRootHex,
1685
+ },
1686
+ e as Error
1687
+ );
1688
+ }
1689
+ }
1690
+
1662
1691
  // Separate boundary: a pool insertion error above must not skip the fork-choice vote
1663
1692
  if (!options.dontSendGossipAttestationsToForkchoice) {
1664
1693
  try {
@@ -20,6 +20,7 @@ const gossipType = GossipType.beacon_attestation;
20
20
  export enum SubnetSource {
21
21
  committee = "committee",
22
22
  longLived = "long_lived",
23
+ beforeProposal = "before_proposal",
23
24
  }
24
25
 
25
26
  // map of subnet to time to form stable mesh as seconds, null if not yet formed
@@ -40,7 +41,7 @@ export class AttnetsService implements IAttnetsService {
40
41
  /** Committee subnets - PeerManager must find peers for those */
41
42
  private committeeSubnets = new SubnetMap();
42
43
  /**
43
- * All currently subscribed short-lived subnets, for attestation aggregation
44
+ * All currently subscribed short-lived subnets, for attestation aggregation and before local block proposals
44
45
  * This class will tell gossip to subscribe and un-subscribe
45
46
  * If a value exists for `SubscriptionId` it means that gossip subscription is active in network.gossip
46
47
  */
@@ -52,6 +53,11 @@ export class AttnetsService implements IAttnetsService {
52
53
  * Used to determine if we should process an attestation.
53
54
  */
54
55
  private aggregatorSlotSubnet = new MapDef<Slot, AggregatorDutyInfo>(() => new Map());
56
+ /**
57
+ * Block proposal slots of validators connected to this node.
58
+ * Used to subscribe to all subnets `slotsToSubscribeBeforeProposalDuty` slots before, see onSlot
59
+ */
60
+ private localProposalSlots = new Set<Slot>();
55
61
 
56
62
  constructor(
57
63
  private readonly config: BeaconConfig,
@@ -119,6 +125,22 @@ export class AttnetsService implements IAttnetsService {
119
125
  }
120
126
  }
121
127
 
128
+ /**
129
+ * Called from the main thread with upcoming block proposal slots of connected validators.
130
+ * Tracked here, the subscription happens in onSlot like aggregator duties, or right away for a proposal learned
131
+ * after its subscription slot (node restart, newly connected validator)
132
+ */
133
+ addLocalProposalSlots(slots: Slot[]): void {
134
+ const clockSlot = this.clock.currentSlot;
135
+ for (const proposalSlot of slots) {
136
+ this.localProposalSlots.add(proposalSlot);
137
+ // not from the slot before the proposal, its votes are published before a mesh could form
138
+ if (proposalSlot - this.opts.slotsToSubscribeBeforeProposalDuty <= clockSlot && clockSlot < proposalSlot - 1) {
139
+ this.subscribeAllSubnetsUntilProposal(proposalSlot);
140
+ }
141
+ }
142
+ }
143
+
122
144
  /**
123
145
  * Check if a subscription is still active before handling a gossip object
124
146
  */
@@ -158,7 +180,8 @@ export class AttnetsService implements IAttnetsService {
158
180
 
159
181
  /**
160
182
  * Run per slot.
161
- * - Subscribe to gossip subnets 2 slots in advance
183
+ * - Subscribe to gossip subnets 2 slots in advance of aggregator duties
184
+ * - Subscribe to all subnets `slotsToSubscribeBeforeProposalDuty` slots in advance of local block proposals
162
185
  * - Unsubscribe from expired subnets
163
186
  * - Track time to stable mesh if not yet formed
164
187
  */
@@ -186,19 +209,76 @@ export class AttnetsService implements IAttnetsService {
186
209
  this.trackTimeToStableMesh(clockSlot, dutiedSlot, dutiedInfo);
187
210
  }
188
211
 
212
+ // Must run before unsubscribeExpiredCommitteeSubnets so that a subnet whose subscription expires in this slot
213
+ // gets its ttl extended instead of being unsubscribed and subscribed again
214
+ this.subscribeAllSubnetsBeforeProposal(clockSlot);
215
+
189
216
  this.unsubscribeExpiredCommitteeSubnets(clockSlot);
190
217
  this.pruneExpiredAggregator(clockSlot);
218
+ this.pruneExpiredLocalProposals(clockSlot);
191
219
  } catch (e) {
192
220
  this.logger.error("Error on AttnetsService.onSlot", {slot: clockSlot}, e as Error);
193
221
  }
194
222
  };
195
223
 
196
224
  private onHalfSlot = (clockSlot: Slot): void => {
197
- for (const [dutiedSlot, dutiedInfo] of this.aggregatorSlotSubnet.entries()) {
198
- this.trackTimeToStableMesh(clockSlot, dutiedSlot, dutiedInfo);
225
+ try {
226
+ for (const [dutiedSlot, dutiedInfo] of this.aggregatorSlotSubnet.entries()) {
227
+ this.trackTimeToStableMesh(clockSlot, dutiedSlot, dutiedInfo);
228
+ }
229
+
230
+ this.unsubscribeAllSubnetsBeforeProposal(clockSlot);
231
+ } catch (e) {
232
+ this.logger.error("Error on AttnetsService.onHalfSlot", {slot: clockSlot}, e as Error);
199
233
  }
200
234
  };
201
235
 
236
+ /**
237
+ * Subscribe to all subnets `slotsToSubscribeBeforeProposalDuty` slots before a block proposal of a connected
238
+ * validator, so that single attestations of those slots can be packed into the block. The subscription expires in
239
+ * the slot before the proposal, see unsubscribeAllSubnetsBeforeProposal
240
+ */
241
+ private subscribeAllSubnetsBeforeProposal(clockSlot: Slot): void {
242
+ for (const proposalSlot of this.localProposalSlots) {
243
+ if (proposalSlot === clockSlot + this.opts.slotsToSubscribeBeforeProposalDuty) {
244
+ this.subscribeAllSubnetsUntilProposal(proposalSlot);
245
+ }
246
+ }
247
+ }
248
+
249
+ private subscribeAllSubnetsUntilProposal(proposalSlot: Slot): void {
250
+ if (this.opts.slotsToSubscribeBeforeProposalDuty === 0 || this.opts.subscribeAllSubnets) {
251
+ return;
252
+ }
253
+
254
+ const subnets = Array.from({length: ATTESTATION_SUBNET_COUNT}, (_, subnet) => subnet);
255
+ // Same as aggregator duties: trigger gossip subscription first, then register the subscriptions.
256
+ // Do not add to committeeSubnets: it drives peer discovery, we only use the peers we already have
257
+ this.subscribeToSubnets(subnets, SubnetSource.beforeProposal);
258
+ for (const subnet of subnets) {
259
+ this.shortLivedSubscriptions.request({subnet, toSlot: proposalSlot - 1});
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Unsubscribe from subnets of subscribeAllSubnetsBeforeProposal at half of the slot before the proposal instead of
265
+ * the proposal slot start, so the proposal slot does not carry their traffic. Single attestations are published at
266
+ * 1/3 of the slot so they have arrived by then.
267
+ * Short-lived subscriptions are either aggregator duties or these, an aggregator duty ends in its dutied slot and is
268
+ * kept here (aggregates are due later), it expires at the next slot start in onSlot as before. So only subnets of
269
+ * subscribeAllSubnetsBeforeProposal are unsubscribed here.
270
+ */
271
+ private unsubscribeAllSubnetsBeforeProposal(clockSlot: Slot): void {
272
+ const aggregatorSubnets = this.aggregatorSlotSubnet.get(clockSlot);
273
+ const expiring = this.shortLivedSubscriptions.getExpired(
274
+ clockSlot + 1,
275
+ (subnet) => aggregatorSubnets?.has(subnet) === true
276
+ );
277
+ if (expiring.length > 0) {
278
+ this.unsubscribeSubnets(expiring, clockSlot + 1, SubnetSource.beforeProposal);
279
+ }
280
+ }
281
+
202
282
  /**
203
283
  * Track time to form stable mesh if not yet formed
204
284
  */
@@ -291,8 +371,8 @@ export class AttnetsService implements IAttnetsService {
291
371
  }
292
372
 
293
373
  /**
294
- * Unsubscribe to a committee subnet from subscribedCommitteeSubnets.
295
- * If a random subnet is present, we do not unsubscribe from it.
374
+ * Unsubscribe from short-lived subnets whose subscription expired.
375
+ * If a long-lived subnet is present, we do not unsubscribe from it.
296
376
  */
297
377
  private unsubscribeExpiredCommitteeSubnets(slot: Slot): void {
298
378
  const expired = this.shortLivedSubscriptions.getExpired(slot);
@@ -313,6 +393,14 @@ export class AttnetsService implements IAttnetsService {
313
393
  }
314
394
  }
315
395
 
396
+ private pruneExpiredLocalProposals(currentSlot: Slot): void {
397
+ for (const proposalSlot of this.localProposalSlots) {
398
+ if (currentSlot > proposalSlot) {
399
+ this.localProposalSlots.delete(proposalSlot);
400
+ }
401
+ }
402
+ }
403
+
316
404
  /** Update ENR */
317
405
  private updateMetadata(): void {
318
406
  const subnets = ssz.phase0.AttestationSubnets.defaultValue();
@@ -25,6 +25,8 @@ export type SubnetsService = {
25
25
 
26
26
  export interface IAttnetsService extends SubnetsService {
27
27
  shouldProcess(subnet: SubnetID, slot: Slot): boolean;
28
+ /** Block proposal slots of validators connected to this node, see `slotsToSubscribeBeforeProposalDuty` */
29
+ addLocalProposalSlots(slots: Slot[]): void;
28
30
  }
29
31
 
30
32
  export type RandBetweenFn = (min: number, max: number) => number;
@@ -33,6 +35,11 @@ export type ShuffleFn = <T>(arr: T[]) => T[];
33
35
  export type SubnetsServiceOpts = {
34
36
  subscribeAllSubnets?: boolean;
35
37
  slotsToSubscribeBeforeAggregatorDuty: number;
38
+ /**
39
+ * Subscribe to all attestation subnets this many slots before a block proposal of a connected validator, so that
40
+ * its single attestations can be packed into the block. In `[0, SLOTS_PER_EPOCH]`, 0 disables
41
+ */
42
+ slotsToSubscribeBeforeProposalDuty: number;
36
43
  };
37
44
 
38
45
  export type SubnetsServiceTestOpts = {
@@ -42,7 +42,7 @@ export enum DataColumnEngineResult {
42
42
 
43
43
  export async function getBlobSidecarsFromExecution(
44
44
  config: ChainForkConfig,
45
- executionEngine: IExecutionEngine,
45
+ executionEngine: Pick<IExecutionEngine, "getBlobs">,
46
46
  metrics: Metrics | null,
47
47
  emitter: ChainEventEmitter,
48
48
  blockInput: IBlockInput
@@ -140,7 +140,7 @@ export async function getBlobSidecarsFromExecution(
140
140
  */
141
141
  export async function getDataColumnSidecarsFromExecution(
142
142
  config: ChainForkConfig,
143
- executionEngine: IExecutionEngine,
143
+ executionEngine: Pick<IExecutionEngine, "getBlobs">,
144
144
  emitter: ChainEventEmitter,
145
145
  input: IBlockInput | PayloadEnvelopeInput,
146
146
  metrics: Metrics | null,
@@ -289,7 +289,7 @@ export type RebuildMiss = {slot: Slot; reason: "unavailable" | "mismatch"; error
289
289
  */
290
290
  export async function* reconstructExecutionPayloadEnvelopesByRange(
291
291
  db: IBeaconDb,
292
- executionEngine: IExecutionEngine,
292
+ executionEngine: Pick<IExecutionEngine, "getPayloadBodiesByHashV2">,
293
293
  logger: Logger,
294
294
  metrics: Metrics | null,
295
295
  startSlot: Slot,
@@ -315,7 +315,7 @@ export async function* reconstructExecutionPayloadEnvelopesByRange(
315
315
  * RANGE_UNSERVABLE for it
316
316
  */
317
317
  async function* reconstructBatch(
318
- executionEngine: IExecutionEngine,
318
+ executionEngine: Pick<IExecutionEngine, "getPayloadBodiesByHashV2">,
319
319
  logger: Logger,
320
320
  metrics: Metrics | null,
321
321
  batch: RangeEntry[]
@@ -355,7 +355,7 @@ async function* reconstructBatch(
355
355
  * on its path. Throws ENGINE_UNAVAILABLE only.
356
356
  */
357
357
  export async function reconstructExecutionPayloadEnvelopes(
358
- executionEngine: IExecutionEngine,
358
+ executionEngine: Pick<IExecutionEngine, "getPayloadBodiesByHashV2">,
359
359
  metrics: Metrics | null,
360
360
  headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[]
361
361
  ): Promise<(gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[]> {
@@ -382,7 +382,7 @@ export function isRebuildMiss(result: gloas.SignedExecutionPayloadEnvelope | Reb
382
382
  * incremented once, in this function.
383
383
  */
384
384
  async function reconstructEnvelopesBatch(
385
- executionEngine: IExecutionEngine,
385
+ executionEngine: Pick<IExecutionEngine, "getPayloadBodiesByHashV2">,
386
386
  metrics: Metrics | null,
387
387
  headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[]
388
388
  ): Promise<(gloas.SignedExecutionPayloadEnvelope | RebuildMiss)[]> {