@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
@@ -4,6 +4,7 @@ import {Type} from "@chainsafe/ssz";
4
4
  import {routes} from "@lodestar/api";
5
5
  import {BeaconConfig} from "@lodestar/config";
6
6
  import {
7
+ AncestorStatus,
7
8
  CheckpointWithHex,
8
9
  EpochDifference,
9
10
  ForkChoiceStateGetter,
@@ -20,6 +21,7 @@ import {
20
21
  type ForkPostGloas,
21
22
  GENESIS_SLOT,
22
23
  SLOTS_PER_EPOCH,
24
+ isForkPostFulu,
23
25
  isForkPostGloas,
24
26
  } from "@lodestar/params";
25
27
  import {
@@ -33,6 +35,7 @@ import {
33
35
  isStatePostAltair,
34
36
  isStatePostElectra,
35
37
  isStatePostGloas,
38
+ proposerShufflingDecisionRoot,
36
39
  } from "@lodestar/state-transition";
37
40
  import {
38
41
  BeaconBlock,
@@ -90,8 +93,9 @@ import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToD
90
93
  import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js";
91
94
  import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js";
92
95
  import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js";
93
- import {ChainEvent, ChainEventEmitter} from "./emitter.js";
96
+ import {ChainEvent, ChainEventEmitter, ReorgEventData} from "./emitter.js";
94
97
  import {ForkchoiceCaller, initializeForkChoice} from "./forkChoice/index.js";
98
+ import {ForkChoiceExecutionEngine} from "./forkChoiceExecutionEngine.js";
95
99
  import {GetBlobsTracker} from "./GetBlobsTracker.js";
96
100
  import {CommonBlockBody, FindHeadFnName, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js";
97
101
  import {LightClientServer} from "./lightClient/index.js";
@@ -104,6 +108,7 @@ import {
104
108
  OpPool,
105
109
  PayloadAttestationPool,
106
110
  ProposerPreferencesPool,
111
+ SingleAttestationPool,
107
112
  SyncCommitteeMessagePool,
108
113
  SyncContributionAndProofPool,
109
114
  } from "./opPools/index.js";
@@ -112,6 +117,7 @@ import {PrepareNextSlotScheduler} from "./prepareNextSlot.js";
112
117
  import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js";
113
118
  import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js";
114
119
  import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js";
120
+ import {ProposerTracker} from "./proposerTracker.js";
115
121
  import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js";
116
122
  import {ReprocessController} from "./reprocess.js";
117
123
  import {
@@ -166,7 +172,7 @@ const DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES = 16;
166
172
  export class BeaconChain implements IBeaconChain {
167
173
  readonly genesisTime: UintNum64;
168
174
  readonly genesisValidatorsRoot: Root;
169
- readonly executionEngine: IExecutionEngine;
175
+ readonly executionEngine: ForkChoiceExecutionEngine;
170
176
  readonly executionBuilder?: IExecutionBuilder;
171
177
  readonly builderCircuitBreaker: BuilderCircuitBreaker;
172
178
  readonly builderApiClient: BuilderApiClient;
@@ -194,6 +200,13 @@ export class BeaconChain implements IBeaconChain {
194
200
  // Ops pool
195
201
  readonly attestationPool: AttestationPool;
196
202
  readonly aggregatedAttestationPool: AggregatedAttestationPool;
203
+ readonly singleAttestationPool: SingleAttestationPool;
204
+ readonly proposerTracker: ProposerTracker;
205
+ /**
206
+ * Head state epoch and decision root of its next epoch proposers when local proposals were last refreshed. The root
207
+ * alone is not enough: a reorg onto a branch without blocks in a whole epoch keeps the root but moves the epoch
208
+ */
209
+ private localProposalsKey: string | null = null;
197
210
  readonly syncCommitteeMessagePool: SyncCommitteeMessagePool;
198
211
  readonly syncContributionAndProofPool;
199
212
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;
@@ -317,7 +330,6 @@ export class BeaconChain implements IBeaconChain {
317
330
  this.genesisTime = anchorState.genesisTime;
318
331
  this.anchorStateLatestBlockSlot = anchorState.latestBlockHeader.slot;
319
332
  this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot;
320
- this.executionEngine = executionEngine;
321
333
  this.executionBuilder = executionBuilder;
322
334
  const signal = this.abortController.signal;
323
335
  const emitter = new ChainEventEmitter();
@@ -331,6 +343,8 @@ export class BeaconChain implements IBeaconChain {
331
343
  this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null]));
332
344
  this.attestationPool = new AttestationPool(config, clock, this.opts?.preaggregateSlotDistance, metrics);
333
345
  this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config, metrics, logger);
346
+ this.singleAttestationPool = new SingleAttestationPool(metrics);
347
+ this.proposerTracker = new ProposerTracker(this.opts.singleAttestationIgnoreProposalDuty ?? false, metrics);
334
348
  this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(config, clock, this.opts?.preaggregateSlotDistance);
335
349
  this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger);
336
350
  this.executionPayloadBidPool = new ExecutionPayloadBidPool();
@@ -457,6 +471,12 @@ export class BeaconChain implements IBeaconChain {
457
471
  this.payloadEnvelopeProcessor = new PayloadEnvelopeProcessor(this, metrics, signal);
458
472
 
459
473
  this.forkChoice = forkChoice;
474
+ this.executionEngine = new ForkChoiceExecutionEngine(executionEngine, {
475
+ forkChoice,
476
+ recomputeHeadAfterELInvalidResponse: () => this.recomputeHeadAfterELInvalidResponse(),
477
+ logger,
478
+ metrics,
479
+ });
460
480
 
461
481
  this.builderCircuitBreaker = new BuilderCircuitBreaker(
462
482
  {faultInspectionWindow: opts.faultInspectionWindow, allowedFaults: opts.allowedFaults},
@@ -1430,6 +1450,58 @@ export class BeaconChain implements IBeaconChain {
1430
1450
  }
1431
1451
  }
1432
1452
 
1453
+ /**
1454
+ * Apply chain side effects of a fork choice head change: head state, head metrics, and reorg detection
1455
+ */
1456
+ onHeadChanged(oldHead: ProtoBlock, newHead: ProtoBlock, maybeHeadState?: IBeaconStateView): void {
1457
+ // Set head state as strong reference
1458
+ this.regen.updateHeadState(newHead, maybeHeadState);
1459
+
1460
+ const delaySec = this.clock.secFromSlot(newHead.slot);
1461
+ this.logger.verbose("New chain head", {
1462
+ slot: newHead.slot,
1463
+ root: newHead.blockRoot,
1464
+ payloadStatus: newHead.payloadStatus,
1465
+ delaySec,
1466
+ });
1467
+ this.metrics?.headSlot.set(newHead.slot);
1468
+
1469
+ this.onNewHead(newHead);
1470
+ this.metrics?.forkChoice.changedHead.inc();
1471
+
1472
+ const ancestorResult = this.forkChoice.getCommonAncestorDepth(oldHead, newHead);
1473
+ if (ancestorResult.code === AncestorStatus.CommonAncestor) {
1474
+ // CommonAncestor = chain reorg, old head and new head not direct descendants
1475
+
1476
+ const forkChoiceReorgEventData: ReorgEventData = {
1477
+ depth: ancestorResult.depth,
1478
+ epoch: computeEpochAtSlot(newHead.slot),
1479
+ slot: newHead.slot,
1480
+ newHeadBlock: newHead.blockRoot,
1481
+ oldHeadBlock: oldHead.blockRoot,
1482
+ newHeadState: newHead.stateRoot,
1483
+ oldHeadState: oldHead.stateRoot,
1484
+ executionOptimistic: isOptimisticBlock(newHead),
1485
+ };
1486
+
1487
+ this.emitter.emit(routes.events.EventType.chainReorg, forkChoiceReorgEventData);
1488
+ this.logger.verbose("Chain reorg", forkChoiceReorgEventData);
1489
+
1490
+ this.metrics?.forkChoice.reorg.inc();
1491
+ this.metrics?.forkChoice.reorgDistance.observe(ancestorResult.depth);
1492
+ }
1493
+ }
1494
+
1495
+ private recomputeHeadAfterELInvalidResponse(): void {
1496
+ // validateLatestHash only re-scores proto-array, the cached head is still the old one
1497
+ const oldHead = this.forkChoice.getHead();
1498
+ const newHead = this.recomputeForkChoiceHead(ForkchoiceCaller.executionInvalid);
1499
+ if (newHead.blockRoot === oldHead.blockRoot && newHead.payloadStatus === oldHead.payloadStatus) {
1500
+ return;
1501
+ }
1502
+ this.onHeadChanged(oldHead, newHead);
1503
+ }
1504
+
1433
1505
  /**
1434
1506
  * Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
1435
1507
  * can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
@@ -1829,6 +1901,8 @@ export class BeaconChain implements IBeaconChain {
1829
1901
 
1830
1902
  this.attestationPool.prune(slot);
1831
1903
  this.aggregatedAttestationPool.prune(slot);
1904
+ this.proposerTracker.prune(slot);
1905
+ this.singleAttestationPool.prune(this.proposerTracker);
1832
1906
  this.syncCommitteeMessagePool.prune(slot);
1833
1907
  this.seenSyncCommitteeMessages.prune(slot);
1834
1908
  this.payloadAttestationPool.prune(slot);
@@ -1855,6 +1929,55 @@ export class BeaconChain implements IBeaconChain {
1855
1929
  }
1856
1930
  }
1857
1931
 
1932
+ /**
1933
+ * Track upcoming proposals of validators connected to this node, so that SingleAttestationPool only collects and
1934
+ * the network only subscribes to all attestation subnets before them. Proposals only change when the decision block
1935
+ * of next epoch proposers changes (the head enters a new epoch or reorgs that block) or a new validator connects,
1936
+ * set localProposalsKey to null before calling for the latter.
1937
+ * Never throws, it runs during block import.
1938
+ */
1939
+ private refreshLocalProposals(): void {
1940
+ const clockSlot = this.clock.currentSlot;
1941
+ try {
1942
+ const headState = this.getHeadState();
1943
+ const headFork = this.config.getForkName(headState.slot);
1944
+ // post-fulu, the head state knows proposers of its epoch and the next one thanks to the proposer lookahead,
1945
+ // a pre-fulu state only predicts next epoch proposers
1946
+ if (!isForkPostFulu(headFork) || headState.epoch < computeEpochAtSlot(clockSlot) - 1) {
1947
+ return;
1948
+ }
1949
+
1950
+ const decisionRoot = proposerShufflingDecisionRoot(headFork, headState, headState.epoch + 1);
1951
+ if (decisionRoot === null) {
1952
+ return;
1953
+ }
1954
+ const localProposalsKey = `${headState.epoch}_${toRootHex(decisionRoot)}`;
1955
+ if (localProposalsKey === this.localProposalsKey) {
1956
+ return;
1957
+ }
1958
+ this.localProposalsKey = localProposalsKey;
1959
+
1960
+ const startSlot = computeStartSlotAtEpoch(headState.epoch);
1961
+ const newProposalSlots: Slot[] = [];
1962
+ for (const [i, proposerIndex] of [...headState.currentProposers, ...headState.nextProposers].entries()) {
1963
+ const proposalSlot = startSlot + i;
1964
+ if (
1965
+ proposalSlot >= clockSlot &&
1966
+ this.beaconProposerCache.has(proposerIndex) &&
1967
+ this.proposerTracker.addProposal(proposalSlot)
1968
+ ) {
1969
+ newProposalSlots.push(proposalSlot);
1970
+ }
1971
+ }
1972
+
1973
+ if (newProposalSlots.length > 0) {
1974
+ this.emitter.emit(ChainEvent.localProposalSlots, newProposalSlots);
1975
+ }
1976
+ } catch (e) {
1977
+ this.logger.debug("Error refreshing local proposals", {slot: clockSlot}, e as Error);
1978
+ }
1979
+ }
1980
+
1858
1981
  private onClockEpoch(epoch: Epoch): void {
1859
1982
  this.metrics?.clockEpoch.set(epoch);
1860
1983
 
@@ -1869,6 +1992,8 @@ export class BeaconChain implements IBeaconChain {
1869
1992
  protected onNewHead(head: ProtoBlock): void {
1870
1993
  this.syncContributionAndProofPool.prune(head.slot);
1871
1994
  this.seenContributionAndProof.prune(head.slot);
1995
+
1996
+ this.refreshLocalProposals();
1872
1997
  }
1873
1998
 
1874
1999
  private onForkChoiceJustified(this: BeaconChain, cp: CheckpointWithHex): void {
@@ -1937,6 +2062,9 @@ export class BeaconChain implements IBeaconChain {
1937
2062
 
1938
2063
  // Only update validator custody if we discovered new validators
1939
2064
  if (newValidatorCount > previousValidatorCount) {
2065
+ // new validators may propose in the current or next epoch, a failed attempt leaves null so the next head retries
2066
+ this.localProposalsKey = null;
2067
+ this.refreshLocalProposals();
1940
2068
  const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint();
1941
2069
  await this.updateValidatorsCustodyRequirement(finalizedCheckpoint);
1942
2070
  }
@@ -1952,6 +2080,9 @@ export class BeaconChain implements IBeaconChain {
1952
2080
 
1953
2081
  // Only update validator custody if we discovered new validators
1954
2082
  if (hasNewValidators) {
2083
+ // new validators may propose in the current or next epoch, a failed attempt leaves null so the next head retries
2084
+ this.localProposalsKey = null;
2085
+ this.refreshLocalProposals();
1955
2086
  const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint();
1956
2087
  await this.updateValidatorsCustodyRequirement(finalizedCheckpoint);
1957
2088
  }
@@ -60,6 +60,11 @@ export enum ChainEvent {
60
60
  * and is ready to be published.
61
61
  */
62
62
  publishProposerSlashing = "publishProposerSlashing",
63
+ /**
64
+ * New block proposal slots of validators connected to this node, the network subscribes to all attestation
65
+ * subnets before them, see `slotsToSubscribeBeforeProposalDuty`
66
+ */
67
+ localProposalSlots = "localProposalSlots",
63
68
  /**
64
69
  * Trigger an update of status so reqresp by peers have current earliestAvailableSlot
65
70
  */
@@ -127,6 +132,8 @@ export type IChainEvents = ApiEvents & {
127
132
 
128
133
  [ChainEvent.publishProposerSlashing]: (proposerSlashing: phase0.ProposerSlashing) => void;
129
134
 
135
+ [ChainEvent.localProposalSlots]: (proposalSlots: Slot[]) => void;
136
+
130
137
  [ChainEvent.updateStatus]: () => void;
131
138
 
132
139
  // Sync events that are chain->chain. Initiated from network requests but do not cross the network
@@ -34,6 +34,17 @@ export type ForkChoiceOpts = RawForkChoiceOpts & {
34
34
  export enum ForkchoiceCaller {
35
35
  prepareNextSlot = "prepare_next_slot",
36
36
  importBlock = "import_block",
37
+ executionInvalid = "execution_invalid",
38
+ }
39
+
40
+ /**
41
+ * Where an execution engine INVALID verdict comes from. Only `blockSegment` verdicts are about the payload the engine
42
+ * just processed and keep the consensus failure semantics in fork choice, the others are applied leniently.
43
+ */
44
+ export enum ExecutionInvalidSource {
45
+ forkchoiceUpdated = "forkchoice_updated",
46
+ newPayload = "new_payload",
47
+ blockSegment = "block_segment",
37
48
  }
38
49
 
39
50
  /**
@@ -0,0 +1,197 @@
1
+ import {ExecutionStatus, IForkChoice, LVHInvalidResponse} from "@lodestar/fork-choice";
2
+ import {ForkName, ForkPostFulu, ForkPreFulu, isForkPostFulu} from "@lodestar/params";
3
+ import {RootHex, bellatrix} from "@lodestar/types";
4
+ import {BlobAndProof} from "@lodestar/types/deneb";
5
+ import {BlobAndProofV2} from "@lodestar/types/fulu";
6
+ import {Logger} from "@lodestar/utils";
7
+ import {ExecutionPayloadBodyV2} from "../execution/engine/types.js";
8
+ import {DATA} from "../execution/engine/utils.js";
9
+ import {
10
+ ClientVersion,
11
+ ExecutionEngineState,
12
+ ExecutionPayloadStatus,
13
+ ForkchoiceUpdatedResult,
14
+ IExecutionEngine,
15
+ PayloadAttributes,
16
+ PayloadId,
17
+ PayloadIdCache,
18
+ VersionedHashes,
19
+ } from "../execution/index.js";
20
+ import {Metrics} from "../metrics/index.js";
21
+ import {ExecutionInvalidSource} from "./forkChoice/index.js";
22
+
23
+ export type ForkChoiceExecutionEngineModules = {
24
+ forkChoice: IForkChoice;
25
+ /** Recompute and switch the chain head, fork choice is already updated when this runs */
26
+ recomputeHeadAfterELInvalidResponse: () => void;
27
+ logger: Logger;
28
+ metrics: Metrics | null;
29
+ };
30
+
31
+ /**
32
+ * The only handle on the execution engine. Payloads are imported optimistically, so every engine verdict that can
33
+ * change fork choice is applied here.
34
+ */
35
+ export class ForkChoiceExecutionEngine {
36
+ private readonly forkChoice: IForkChoice;
37
+ private readonly recomputeHeadAfterELInvalidResponse: () => void;
38
+ private readonly logger: Logger;
39
+ private readonly metrics: Metrics | null;
40
+
41
+ constructor(
42
+ private readonly engine: IExecutionEngine,
43
+ modules: ForkChoiceExecutionEngineModules
44
+ ) {
45
+ this.forkChoice = modules.forkChoice;
46
+ this.recomputeHeadAfterELInvalidResponse = modules.recomputeHeadAfterELInvalidResponse;
47
+ this.logger = modules.logger;
48
+ this.metrics = modules.metrics;
49
+ }
50
+
51
+ get state(): ExecutionEngineState {
52
+ return this.engine.state;
53
+ }
54
+
55
+ get clientVersion(): ClientVersion | null | undefined {
56
+ return this.engine.clientVersion;
57
+ }
58
+
59
+ get payloadIdCache(): PayloadIdCache {
60
+ return this.engine.payloadIdCache;
61
+ }
62
+
63
+ notifyNewPayload(
64
+ ...args: Parameters<IExecutionEngine["notifyNewPayload"]>
65
+ ): ReturnType<IExecutionEngine["notifyNewPayload"]> {
66
+ return this.engine.notifyNewPayload(...args);
67
+ }
68
+
69
+ /**
70
+ * `headBlockRoot` is the root of the node whose payload hash is sent as `headBlockHash`, captured by the caller
71
+ * before the call: root and hash identify the exact fork choice node, gloas variant included.
72
+ *
73
+ * The verdict is applied to fork choice before returning. An INVALID verdict throws when preparing a payload so
74
+ * block production fails, otherwise it resolves to null.
75
+ */
76
+ async notifyForkchoiceUpdate(
77
+ fork: ForkName,
78
+ headBlockRoot: RootHex,
79
+ headBlockHash: RootHex,
80
+ safeBlockHash: RootHex,
81
+ finalizedBlockHash: RootHex,
82
+ payloadAttributes?: PayloadAttributes
83
+ ): Promise<PayloadId | null> {
84
+ const result = await this.engine.notifyForkchoiceUpdate(
85
+ fork,
86
+ headBlockHash,
87
+ safeBlockHash,
88
+ finalizedBlockHash,
89
+ payloadAttributes
90
+ );
91
+ this.applyForkchoiceUpdatedResult(headBlockRoot, headBlockHash, result);
92
+
93
+ if (result.payloadStatus.status === ExecutionPayloadStatus.INVALID) {
94
+ if (payloadAttributes) {
95
+ throw Error(`Invalid prepare payload, validationError=${result.payloadStatus.validationError ?? ""}`);
96
+ }
97
+ return null;
98
+ }
99
+ return result.payloadId;
100
+ }
101
+
102
+ getPayload(...args: Parameters<IExecutionEngine["getPayload"]>): ReturnType<IExecutionEngine["getPayload"]> {
103
+ return this.engine.getPayload(...args);
104
+ }
105
+
106
+ getInclusionList(): Promise<bellatrix.Transactions> {
107
+ return this.engine.getInclusionList();
108
+ }
109
+
110
+ getPayloadBodiesByHashV2(blockHashes: DATA[]): Promise<(ExecutionPayloadBodyV2 | null)[]> {
111
+ return this.engine.getPayloadBodiesByHashV2(blockHashes);
112
+ }
113
+
114
+ getBlobs(
115
+ fork: ForkPostFulu,
116
+ versionedHashes: VersionedHashes,
117
+ buffers?: Uint8Array[]
118
+ ): Promise<BlobAndProofV2[] | null>;
119
+ getBlobs(
120
+ fork: ForkPreFulu,
121
+ versionedHashes: VersionedHashes,
122
+ buffers?: Uint8Array[]
123
+ ): Promise<(BlobAndProof | null)[]>;
124
+ getBlobs(
125
+ fork: ForkName,
126
+ versionedHashes: VersionedHashes,
127
+ buffers?: Uint8Array[]
128
+ ): Promise<BlobAndProofV2[] | (BlobAndProof | null)[] | null> {
129
+ if (isForkPostFulu(fork)) {
130
+ return this.engine.getBlobs(fork, versionedHashes, buffers);
131
+ }
132
+ return this.engine.getBlobs(fork as ForkPreFulu, versionedHashes, buffers);
133
+ }
134
+
135
+ /**
136
+ * Apply an execution engine INVALID verdict to fork choice and move the chain head off the invalid chain
137
+ */
138
+ invalidateExecutionPayloads(lvh: LVHInvalidResponse, source: ExecutionInvalidSource): void {
139
+ const logCtx = {
140
+ source,
141
+ blockRoot: lvh.invalidateFromParentBlockRoot,
142
+ blockHash: lvh.invalidateFromParentBlockHash,
143
+ latestValidHash: lvh.latestValidExecHash ?? "null",
144
+ };
145
+ const applied = this.forkChoice.validateLatestHash(lvh, {strict: source === ExecutionInvalidSource.blockSegment});
146
+ if (!applied) {
147
+ this.logger.warn("Execution engine INVALID verdict not applied to fork choice", logCtx);
148
+ return;
149
+ }
150
+ this.metrics?.forkChoice.executionInvalidations.inc({source});
151
+ this.logger.warn("Invalidated execution payloads in fork choice", logCtx);
152
+
153
+ try {
154
+ this.recomputeHeadAfterELInvalidResponse();
155
+ } catch (e) {
156
+ this.logger.error("Error recomputing head after execution payload invalidation", logCtx, e as Error);
157
+ }
158
+ }
159
+
160
+ private applyForkchoiceUpdatedResult(
161
+ headBlockRoot: RootHex,
162
+ headBlockHash: RootHex,
163
+ result: ForkchoiceUpdatedResult
164
+ ): void {
165
+ const {status, latestValidHash, validationError} = result.payloadStatus;
166
+ const executionStatus = this.forkChoice.getBlockHexAndBlockHash(headBlockRoot, headBlockHash)?.executionStatus;
167
+ try {
168
+ if (status === ExecutionPayloadStatus.INVALID) {
169
+ if (executionStatus === ExecutionStatus.Invalid) {
170
+ return;
171
+ }
172
+ this.logger.error("Execution engine reported INVALID forkchoice head", {
173
+ headBlockRoot,
174
+ headBlockHash,
175
+ latestValidHash: latestValidHash ?? "null",
176
+ validationError: validationError ?? "",
177
+ });
178
+ this.invalidateExecutionPayloads(
179
+ {
180
+ executionStatus: ExecutionStatus.Invalid,
181
+ latestValidExecHash: latestValidHash,
182
+ invalidateFromParentBlockRoot: headBlockRoot,
183
+ invalidateFromParentBlockHash: headBlockHash,
184
+ },
185
+ ExecutionInvalidSource.forkchoiceUpdated
186
+ );
187
+ } else if (status === ExecutionPayloadStatus.VALID && executionStatus === ExecutionStatus.Syncing) {
188
+ this.forkChoice.validateLatestHash(
189
+ {executionStatus: ExecutionStatus.Valid, latestValidExecHash: headBlockHash, blockRoot: headBlockRoot},
190
+ {strict: false}
191
+ );
192
+ }
193
+ } catch (e) {
194
+ this.logger.error("Error applying forkchoice update result", {headBlockRoot, headBlockHash, status}, e as Error);
195
+ }
196
+ }
197
+ }
@@ -25,7 +25,7 @@ import {
25
25
  } from "@lodestar/types";
26
26
  import {Logger} from "@lodestar/utils";
27
27
  import {BuilderApiClient} from "../execution/builder/apiClient.js";
28
- import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js";
28
+ import {IExecutionBuilder} from "../execution/index.js";
29
29
  import {Metrics} from "../metrics/metrics.js";
30
30
  import {BufferPool} from "../util/bufferPool.js";
31
31
  import {IClock} from "../util/clock.js";
@@ -42,6 +42,7 @@ import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js";
42
42
  import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js";
43
43
  import {ChainEventEmitter} from "./emitter.js";
44
44
  import {ForkchoiceCaller} from "./forkChoice/index.js";
45
+ import {ForkChoiceExecutionEngine} from "./forkChoiceExecutionEngine.js";
45
46
  import {GetBlobsTracker} from "./GetBlobsTracker.js";
46
47
  import {LightClientServer} from "./lightClient/index.js";
47
48
  import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool.js";
@@ -53,11 +54,13 @@ import {
53
54
  OpPool,
54
55
  PayloadAttestationPool,
55
56
  ProposerPreferencesPool,
57
+ SingleAttestationPool,
56
58
  SyncCommitteeMessagePool,
57
59
  SyncContributionAndProofPool,
58
60
  } from "./opPools/index.js";
59
61
  import {IChainOptions} from "./options.js";
60
62
  import {AssembledBlockType, BlockAttributes, BlockType, ProduceResult} from "./produceBlock/produceBlockBody.js";
63
+ import {ProposerTracker} from "./proposerTracker.js";
61
64
  import {IStateRegenerator, RegenCaller} from "./regen/index.js";
62
65
  import {ReprocessController} from "./reprocess.js";
63
66
  import {
@@ -98,7 +101,7 @@ export interface IBeaconChain {
98
101
  readonly genesisTime: UintNum64;
99
102
  readonly genesisValidatorsRoot: Root;
100
103
  earliestAvailableSlot: Slot;
101
- readonly executionEngine: IExecutionEngine;
104
+ readonly executionEngine: ForkChoiceExecutionEngine;
102
105
  readonly executionBuilder?: IExecutionBuilder;
103
106
  readonly builderCircuitBreaker: BuilderCircuitBreaker;
104
107
  readonly builderApiClient: BuilderApiClient;
@@ -126,6 +129,8 @@ export interface IBeaconChain {
126
129
  // Ops pool
127
130
  readonly attestationPool: AttestationPool;
128
131
  readonly aggregatedAttestationPool: AggregatedAttestationPool;
132
+ readonly singleAttestationPool: SingleAttestationPool;
133
+ readonly proposerTracker: ProposerTracker;
129
134
  readonly syncCommitteeMessagePool: SyncCommitteeMessagePool;
130
135
  readonly syncContributionAndProofPool: SyncContributionAndProofPool;
131
136
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;