@lodestar/fork-choice 1.44.0-dev.6ef8199cfa → 1.44.0-dev.762dd5f186

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 (73) hide show
  1. package/lib/forkChoice/fastConfirmation/data.d.ts +4 -0
  2. package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -0
  3. package/lib/forkChoice/fastConfirmation/data.js +31 -0
  4. package/lib/forkChoice/fastConfirmation/data.js.map +1 -0
  5. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts +17 -0
  6. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts.map +1 -0
  7. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js +139 -0
  8. package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js.map +1 -0
  9. package/lib/forkChoice/fastConfirmation/index.d.ts +4 -0
  10. package/lib/forkChoice/fastConfirmation/index.d.ts.map +1 -0
  11. package/lib/forkChoice/fastConfirmation/index.js +4 -0
  12. package/lib/forkChoice/fastConfirmation/index.js.map +1 -0
  13. package/lib/forkChoice/fastConfirmation/metrics.d.ts +24 -0
  14. package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -0
  15. package/lib/forkChoice/fastConfirmation/metrics.js +55 -0
  16. package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -0
  17. package/lib/forkChoice/fastConfirmation/rules.d.ts +9 -0
  18. package/lib/forkChoice/fastConfirmation/rules.d.ts.map +1 -0
  19. package/lib/forkChoice/fastConfirmation/rules.js +114 -0
  20. package/lib/forkChoice/fastConfirmation/rules.js.map +1 -0
  21. package/lib/forkChoice/fastConfirmation/types.d.ts +115 -0
  22. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -0
  23. package/lib/forkChoice/fastConfirmation/types.js +22 -0
  24. package/lib/forkChoice/fastConfirmation/types.js.map +1 -0
  25. package/lib/forkChoice/fastConfirmation/utils.d.ts +47 -0
  26. package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -0
  27. package/lib/forkChoice/fastConfirmation/utils.js +687 -0
  28. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -0
  29. package/lib/forkChoice/forkChoice.d.ts +24 -4
  30. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  31. package/lib/forkChoice/forkChoice.js +151 -35
  32. package/lib/forkChoice/forkChoice.js.map +1 -1
  33. package/lib/forkChoice/interface.d.ts +21 -8
  34. package/lib/forkChoice/interface.d.ts.map +1 -1
  35. package/lib/forkChoice/interface.js.map +1 -1
  36. package/lib/forkChoice/safeBlocks.d.ts +2 -6
  37. package/lib/forkChoice/safeBlocks.d.ts.map +1 -1
  38. package/lib/forkChoice/safeBlocks.js +15 -7
  39. package/lib/forkChoice/safeBlocks.js.map +1 -1
  40. package/lib/forkChoice/store.d.ts +33 -2
  41. package/lib/forkChoice/store.d.ts.map +1 -1
  42. package/lib/forkChoice/store.js +37 -1
  43. package/lib/forkChoice/store.js.map +1 -1
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +1 -0
  47. package/lib/index.js.map +1 -1
  48. package/lib/metrics.d.ts +15 -1
  49. package/lib/metrics.d.ts.map +1 -1
  50. package/lib/metrics.js +2 -0
  51. package/lib/metrics.js.map +1 -1
  52. package/lib/protoArray/interface.d.ts +1 -0
  53. package/lib/protoArray/interface.d.ts.map +1 -1
  54. package/lib/protoArray/protoArray.d.ts +68 -21
  55. package/lib/protoArray/protoArray.d.ts.map +1 -1
  56. package/lib/protoArray/protoArray.js +172 -39
  57. package/lib/protoArray/protoArray.js.map +1 -1
  58. package/package.json +7 -7
  59. package/src/forkChoice/fastConfirmation/data.ts +43 -0
  60. package/src/forkChoice/fastConfirmation/fastConfirmationRule.ts +173 -0
  61. package/src/forkChoice/fastConfirmation/index.ts +3 -0
  62. package/src/forkChoice/fastConfirmation/metrics.ts +57 -0
  63. package/src/forkChoice/fastConfirmation/rules.ts +151 -0
  64. package/src/forkChoice/fastConfirmation/types.ts +137 -0
  65. package/src/forkChoice/fastConfirmation/utils.ts +975 -0
  66. package/src/forkChoice/forkChoice.ts +189 -37
  67. package/src/forkChoice/interface.ts +38 -7
  68. package/src/forkChoice/safeBlocks.ts +15 -7
  69. package/src/forkChoice/store.ts +45 -1
  70. package/src/index.ts +11 -0
  71. package/src/metrics.ts +3 -1
  72. package/src/protoArray/interface.ts +8 -0
  73. package/src/protoArray/protoArray.ts +186 -41
@@ -25,7 +25,7 @@ import {
25
25
  phase0,
26
26
  ssz,
27
27
  } from "@lodestar/types";
28
- import {Logger, MapDef, fromHex, toRootHex} from "@lodestar/utils";
28
+ import {Logger, MapDef, fromHex, toRootHex, withObservedDuration} from "@lodestar/utils";
29
29
  import {ForkChoiceMetrics} from "../metrics.js";
30
30
  import {computeDeltas} from "../protoArray/computeDeltas.js";
31
31
  import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";
@@ -44,6 +44,12 @@ import {
44
44
  } from "../protoArray/interface.js";
45
45
  import {ProtoArray} from "../protoArray/protoArray.js";
46
46
  import {ForkChoiceError, ForkChoiceErrorCode, InvalidAttestationCode, InvalidBlockCode} from "./errors.js";
47
+ import {
48
+ type FastConfirmationContext,
49
+ FastConfirmationRule,
50
+ FastConfirmationSteps,
51
+ type IFastConfirmationRule,
52
+ } from "./fastConfirmation/fastConfirmationRule.ts";
47
53
  import {
48
54
  AncestorResult,
49
55
  AncestorStatus,
@@ -58,6 +64,7 @@ export type ForkChoiceOpts = {
58
64
  proposerBoost?: boolean;
59
65
  proposerBoostReorg?: boolean;
60
66
  computeUnrealized?: boolean;
67
+ fastConfirmation?: boolean;
61
68
  };
62
69
 
63
70
  export enum UpdateHeadOpt {
@@ -142,6 +149,9 @@ export class ForkChoice implements IForkChoice {
142
149
  private justifiedProposerBoostScore: number | null = null;
143
150
  /** The current effective balances */
144
151
  private balances: EffectiveBalanceIncrements;
152
+ /** Optional fast confirmation rule implementation */
153
+ private readonly fastConfirmationRule?: IFastConfirmationRule;
154
+ private readonly fastConfirmationContext?: FastConfirmationContext;
145
155
  /**
146
156
  * Instantiates a Fork Choice from some existing components
147
157
  *
@@ -167,6 +177,11 @@ export class ForkChoice implements IForkChoice {
167
177
  this.head = this.updateHead();
168
178
  this.balances = this.fcStore.justified.balances;
169
179
 
180
+ if (this.opts?.fastConfirmation) {
181
+ this.fastConfirmationRule = new FastConfirmationRule(this.fcStore, metrics, this.logger);
182
+ this.fastConfirmationContext = this.createFastConfirmationContext();
183
+ }
184
+
170
185
  metrics?.forkChoice.votes.addCollect(() => {
171
186
  metrics.forkChoice.votes.set(this.voteNextSlots.length);
172
187
  metrics.forkChoice.queuedAttestations.set(this.queuedAttestationsPreviousSlot);
@@ -207,6 +222,14 @@ export class ForkChoice implements IForkChoice {
207
222
  return this.head;
208
223
  }
209
224
 
225
+ getConfirmedRoot(): RootHex {
226
+ return this.fastConfirmationRule?.getConfirmedRoot() ?? this.fcStore.justified.checkpoint.rootHex;
227
+ }
228
+
229
+ getConfirmedBlock(): ProtoBlock | null {
230
+ return this.getBlockHexDefaultStatus(this.getConfirmedRoot());
231
+ }
232
+
210
233
  /**
211
234
  *
212
235
  * A multiplexer to wrap around the traditional `updateHead()` according to the scenario
@@ -310,6 +333,10 @@ export class ForkChoice implements IForkChoice {
310
333
  return this.proposerBoostRoot ?? HEX_ZERO_HASH;
311
334
  }
312
335
 
336
+ getPreviousProposerBoostRoot(): RootHex {
337
+ return this.protoArray.getPreviousProposerBoostRoot();
338
+ }
339
+
313
340
  /**
314
341
  * Decides whether to extend an available payload from the previous slot,
315
342
  * corresponding to the beacon block `blockRoot`.
@@ -318,6 +345,11 @@ export class ForkChoice implements IForkChoice {
318
345
  return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot);
319
346
  }
320
347
 
348
+ /** Spec: should_build_on_full(store, head) */
349
+ shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
350
+ return this.protoArray.shouldBuildOnFull(head, slot);
351
+ }
352
+
321
353
  /**
322
354
  * To predict the proposer head of the next slot. That is, to predict if proposer-boost-reorg could happen.
323
355
  * Reason why we can't be certain is because information of the head block is not fully available yet
@@ -596,7 +628,11 @@ export class ForkChoice implements IForkChoice {
596
628
  blockDelaySec: number,
597
629
  currentSlot: Slot,
598
630
  executionStatus: BlockExecutionStatus,
599
- dataAvailabilityStatus: DataAvailabilityStatus
631
+ dataAvailabilityStatus: DataAvailabilityStatus,
632
+ // The expected proposer index on the canonical chain we are following.
633
+ // Calculated by our head state. We use it as part of the proposer
634
+ // boost decision making. No boost will be set if this is null.
635
+ expectedProposerIndex: ValidatorIndex | null
600
636
  ): ProtoBlock {
601
637
  const {parentRoot, slot} = block;
602
638
  const parentRootHex = toRootHex(parentRoot);
@@ -669,7 +705,9 @@ export class ForkChoice implements IForkChoice {
669
705
  this.opts?.proposerBoost &&
670
706
  isTimely &&
671
707
  // only boost the first block we see
672
- this.proposerBoostRoot === null
708
+ this.proposerBoostRoot === null &&
709
+ expectedProposerIndex !== null &&
710
+ block.proposerIndex === expectedProposerIndex
673
711
  ) {
674
712
  this.proposerBoostRoot = blockRootHex;
675
713
  }
@@ -764,33 +802,32 @@ export class ForkChoice implements IForkChoice {
764
802
  unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
765
803
 
766
804
  ...(isGloasBeaconBlock(block)
767
- ? {
768
- executionPayloadBlockHash: toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash), // post-gloas, we don't know payload hash until we import execution payload. Set to parent payload hash for now
769
- executionPayloadNumber: (() => {
770
- // Determine parent's execution payload number based on which variant the block extends
771
- const parentBlockHashFromBid = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash);
772
-
773
- // If parent is pre-merge, return 0
774
- if (parentBlock.executionPayloadBlockHash === null) {
775
- return 0;
776
- }
777
-
778
- // If parent is pre-Gloas, it only has FULL variant
779
- if (parentBlock.parentBlockHash === null) {
780
- return parentBlock.executionPayloadNumber;
781
- }
782
-
783
- // Parent is Gloas: get the variant that matches the parentBlockHash from bid
784
- const parentVariant = this.getBlockHexAndBlockHash(parentRootHex, parentBlockHashFromBid);
785
- if (parentVariant && parentVariant.executionPayloadBlockHash !== null) {
786
- return parentVariant.executionPayloadNumber;
787
- }
788
- // Fallback to parent block's number (we know it's post-merge from check above)
789
- return parentBlock.executionPayloadNumber;
790
- })(),
791
- executionStatus: this.getPostMergeExecStatus(executionStatus),
792
- dataAvailabilityStatus,
793
- }
805
+ ? (() => {
806
+ // post-gloas, we don't know payload hash until we import execution payload. Set to
807
+ // parent payload hash for now, along with the gas limit/number of that parent payload
808
+ // (which is what bids built on top of this block will reference until a payload arrives).
809
+ // we also use parent hash to pass to EL via fcu
810
+ // see https://github.com/ethereum/consensus-specs/pull/5197
811
+ const parentBlockHashFromBid = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash);
812
+
813
+ // Inherit parent payload's (number, gasLimit) for the PENDING/EMPTY variants.
814
+ // `parentBlock` is already the variant matching `parentBlockHashFromBid` —
815
+ // `getParent` (called above) resolves Gloas parents via
816
+ // `getBlockHexAndBlockHash(parentRoot, parentBlockHash)`, and pre-Gloas parents
817
+ // have a single variant. Pre-merge parents have null payload hash and zero values.
818
+ const parentMeta: {number: number; gasLimit: number} =
819
+ parentBlock.executionPayloadBlockHash === null
820
+ ? {number: 0, gasLimit: 0}
821
+ : {number: parentBlock.executionPayloadNumber, gasLimit: parentBlock.executionPayloadGasLimit};
822
+
823
+ return {
824
+ executionPayloadBlockHash: parentBlockHashFromBid,
825
+ executionPayloadNumber: parentMeta.number,
826
+ executionPayloadGasLimit: parentMeta.gasLimit,
827
+ executionStatus: this.getPostMergeExecStatus(executionStatus),
828
+ dataAvailabilityStatus,
829
+ };
830
+ })()
794
831
  : isExecutionBlockBodyType(block.body) &&
795
832
  isStatePostBellatrix(state) &&
796
833
  state.isExecutionStateType &&
@@ -798,6 +835,7 @@ export class ForkChoice implements IForkChoice {
798
835
  ? {
799
836
  executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
800
837
  executionPayloadNumber: block.body.executionPayload.blockNumber,
838
+ executionPayloadGasLimit: block.body.executionPayload.gasLimit,
801
839
  executionStatus: this.getPostMergeExecStatus(executionStatus),
802
840
  dataAvailabilityStatus,
803
841
  }
@@ -934,8 +972,14 @@ export class ForkChoice implements IForkChoice {
934
972
  * Updates the PTC votes for multiple validators attesting to a block
935
973
  * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
936
974
  */
937
- notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void {
938
- this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent);
975
+ notifyPtcMessages(
976
+ blockRoot: RootHex,
977
+ slot: Slot,
978
+ ptcIndices: number[],
979
+ payloadPresent: boolean,
980
+ blobDataAvailable: boolean
981
+ ): void {
982
+ this.protoArray.notifyPtcMessages(blockRoot, slot, ptcIndices, payloadPresent, blobDataAvailable);
939
983
  }
940
984
 
941
985
  /**
@@ -947,6 +991,7 @@ export class ForkChoice implements IForkChoice {
947
991
  blockRoot: RootHex,
948
992
  executionPayloadBlockHash: RootHex,
949
993
  executionPayloadNumber: number,
994
+ executionPayloadGasLimit: number,
950
995
  executionStatus: PayloadExecutionStatus,
951
996
  dataAvailabilityStatus: DataAvailabilityStatus
952
997
  ): void {
@@ -955,6 +1000,7 @@ export class ForkChoice implements IForkChoice {
955
1000
  this.fcStore.currentSlot,
956
1001
  executionPayloadBlockHash,
957
1002
  executionPayloadNumber,
1003
+ executionPayloadGasLimit,
958
1004
  this.proposerBoostRoot,
959
1005
  executionStatus,
960
1006
  dataAvailabilityStatus
@@ -975,12 +1021,12 @@ export class ForkChoice implements IForkChoice {
975
1021
  const previousSlot = this.fcStore.currentSlot;
976
1022
  // Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
977
1023
  this.onTick(previousSlot + 1);
1024
+ this.queuedAttestationsPreviousSlot = 0;
1025
+ // Process any attestations that might now be eligible before running FCR for this slot.
1026
+ this.processAttestationQueue();
1027
+ this.runFastConfirmation();
1028
+ this.validatedAttestationDatas = new Set();
978
1029
  }
979
-
980
- this.queuedAttestationsPreviousSlot = 0;
981
- // Process any attestations that might now be eligible.
982
- this.processAttestationQueue();
983
- this.validatedAttestationDatas = new Set();
984
1030
  }
985
1031
 
986
1032
  getTime(): Slot {
@@ -1049,6 +1095,30 @@ export class ForkChoice implements IForkChoice {
1049
1095
  return votes.toBoolArray().map((v) => v ?? null);
1050
1096
  }
1051
1097
 
1098
+ getPTCVoteCounts(blockRootHex: RootHex): {
1099
+ attesterCount: number;
1100
+ payloadPresentCount: number;
1101
+ dataAvailableCount: number;
1102
+ } | null {
1103
+ return this.protoArray.getPTCVoteCounts(blockRootHex);
1104
+ }
1105
+
1106
+ getPayloadTimelinessVotes(blockRootHex: RootHex): (boolean | null)[] | null {
1107
+ return this.protoArray.getPayloadTimelinessVotes(blockRootHex);
1108
+ }
1109
+
1110
+ getPayloadDataAvailabilityVotes(blockRootHex: RootHex): (boolean | null)[] | null {
1111
+ return this.protoArray.getPayloadDataAvailabilityVotes(blockRootHex);
1112
+ }
1113
+
1114
+ getUnrealizedJustifiedCheckpoint(): CheckpointWithHex {
1115
+ return this.fcStore.unrealizedJustified.checkpoint;
1116
+ }
1117
+
1118
+ getUnrealizedFinalizedCheckpoint(): CheckpointWithHex {
1119
+ return this.fcStore.unrealizedFinalizedCheckpoint;
1120
+ }
1121
+
1052
1122
  /**
1053
1123
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
1054
1124
  */
@@ -1874,6 +1944,88 @@ export class ForkChoice implements IForkChoice {
1874
1944
 
1875
1945
  return {prelimProposerHead};
1876
1946
  }
1947
+
1948
+ private runFastConfirmation(): void {
1949
+ withObservedDuration(this.metrics?.fastConfirmation.totalDuration.startTimer(), () => {
1950
+ if (!this.fastConfirmationRule || !this.fastConfirmationContext) return;
1951
+
1952
+ try {
1953
+ withObservedDuration(
1954
+ this.metrics?.fastConfirmation.stepsDuration.startTimer({
1955
+ step: FastConfirmationSteps.updateHead,
1956
+ }),
1957
+ () => this.updateHead()
1958
+ );
1959
+
1960
+ const result = this.fastConfirmationRule.onSlotStartAfterPastAttestationsApplied(this.fastConfirmationContext);
1961
+ this.fcStore.confirmedRoot = result.confirmedRoot;
1962
+
1963
+ const confirmedBlock = this.getBlockHexDefaultStatus(result.confirmedRoot);
1964
+ if (confirmedBlock === null) {
1965
+ throw new Error(`Fast confirmation produced root not in protoArray: ${result.confirmedRoot}`);
1966
+ }
1967
+ this.fcStore.notifyFastConfirmation?.({
1968
+ block: result.confirmedRoot,
1969
+ slot: confirmedBlock.slot,
1970
+ currentSlot: this.fcStore.currentSlot,
1971
+ });
1972
+ } catch (err) {
1973
+ this.logger?.debug(
1974
+ "Fast confirmation failed",
1975
+ {slot: this.fcStore.currentSlot, head: this.head.blockRoot, confirmedRoot: this.fcStore.confirmedRoot},
1976
+ err as Error
1977
+ );
1978
+ }
1979
+ });
1980
+ }
1981
+
1982
+ private createFastConfirmationContext(): FastConfirmationContext {
1983
+ const confirmationByzantineThreshold = this.config.CONFIRMATION_BYZANTINE_THRESHOLD;
1984
+ if (!confirmationByzantineThreshold) {
1985
+ throw new Error("CONFIRMATION_BYZANTINE_THRESHOLD must be set to use fast confirmation");
1986
+ }
1987
+
1988
+ return {
1989
+ config: {
1990
+ CONFIRMATION_BYZANTINE_THRESHOLD: confirmationByzantineThreshold,
1991
+ PROPOSER_SCORE_BOOST: this.config.PROPOSER_SCORE_BOOST,
1992
+ },
1993
+ getCurrentSlot: () => this.fcStore.currentSlot,
1994
+ getHead: () => this.head,
1995
+ getBlock: (root: RootHex) => this.getBlockHexDefaultStatus(root),
1996
+ getAncestor: (root: RootHex, slot: Slot) => this.getAncestor(root, slot).blockRoot,
1997
+ isDescendant: (ancestor: RootHex, descendant: RootHex) => {
1998
+ const ancestorStatus = this.protoArray.getDefaultVariant(ancestor);
1999
+ const descendantStatus = this.protoArray.getDefaultVariant(descendant);
2000
+ if (ancestorStatus === undefined || descendantStatus === undefined) return false;
2001
+ return this.isDescendant(ancestor, ancestorStatus, descendant, descendantStatus);
2002
+ },
2003
+ getLatestMessage: (validatorIndex: ValidatorIndex) => {
2004
+ const nextIndex = this.voteNextIndices[validatorIndex];
2005
+ if (nextIndex === undefined || nextIndex === NULL_VOTE_INDEX) {
2006
+ return null;
2007
+ }
2008
+ const node = this.protoArray.nodes[nextIndex];
2009
+ if (!node) return null;
2010
+ return {root: node.blockRoot, epoch: computeEpochAtSlot(this.voteNextSlots[validatorIndex])};
2011
+ },
2012
+ getUnrealizedJustified: () => ({
2013
+ checkpoint: this.fcStore.unrealizedJustified.checkpoint,
2014
+ balances: this.fcStore.unrealizedJustified.balances,
2015
+ }),
2016
+ getFinalizedCheckpoint: () => this.fcStore.finalizedCheckpoint,
2017
+ getEquivocatingIndices: () => this.fcStore.equivocatingIndices,
2018
+ getTrackedVotesCount: () => {
2019
+ let count = 0;
2020
+ for (let i = 0; i < this.voteNextIndices.length; i++) {
2021
+ if (this.voteNextIndices[i] !== NULL_VOTE_INDEX) {
2022
+ count++;
2023
+ }
2024
+ }
2025
+ return count;
2026
+ },
2027
+ };
2028
+ }
1877
2029
  }
1878
2030
 
1879
2031
  // Approximate https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
@@ -1,5 +1,14 @@
1
1
  import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
2
- import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types";
2
+ import {
3
+ AttesterSlashing,
4
+ BeaconBlock,
5
+ Epoch,
6
+ IndexedAttestation,
7
+ Root,
8
+ RootHex,
9
+ Slot,
10
+ ValidatorIndex,
11
+ } from "@lodestar/types";
3
12
  import {
4
13
  BlockExecutionStatus,
5
14
  LVHExecResponse,
@@ -98,6 +107,8 @@ export interface IForkChoice {
98
107
  */
99
108
  getHeadRoot(): RootHex;
100
109
  getHead(): ProtoBlock;
110
+ getConfirmedRoot(): RootHex;
111
+ getConfirmedBlock(): ProtoBlock | null;
101
112
  updateAndGetHead(mode: UpdateAndGetHeadOpt): {
102
113
  head: ProtoBlock;
103
114
  isHeadTimely?: boolean;
@@ -123,6 +134,10 @@ export interface IForkChoice {
123
134
  getAllNodes(): ProtoNode[];
124
135
  getFinalizedCheckpoint(): CheckpointWithHex;
125
136
  getJustifiedCheckpoint(): CheckpointWithHex;
137
+ getUnrealizedJustifiedCheckpoint(): CheckpointWithHex;
138
+ getUnrealizedFinalizedCheckpoint(): CheckpointWithHex;
139
+ getProposerBoostRoot(): RootHex;
140
+ getPreviousProposerBoostRoot(): RootHex;
126
141
  /**
127
142
  * Add `block` to the fork choice DAG.
128
143
  *
@@ -145,7 +160,8 @@ export interface IForkChoice {
145
160
  blockDelaySec: number,
146
161
  currentSlot: Slot,
147
162
  executionStatus: BlockExecutionStatus,
148
- dataAvailabilityStatus: DataAvailabilityStatus
163
+ dataAvailabilityStatus: DataAvailabilityStatus,
164
+ expectedProposerIndex: ValidatorIndex | null
149
165
  ): ProtoBlock;
150
166
  /**
151
167
  * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
@@ -181,12 +197,14 @@ export interface IForkChoice {
181
197
  * ## Specification
182
198
  *
183
199
  * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/fork-choice.md#new-notify_ptc_messages
184
- *
185
- * @param blockRoot - The beacon block root being attested
186
- * @param ptcIndices - Array of PTC committee indices that voted
187
- * @param payloadPresent - Whether validators attest the payload is present
188
200
  */
189
- notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void;
201
+ notifyPtcMessages(
202
+ blockRoot: RootHex,
203
+ slot: Slot,
204
+ ptcIndices: number[],
205
+ payloadPresent: boolean,
206
+ blobDataAvailable: boolean
207
+ ): void;
190
208
  /**
191
209
  * Notify fork choice that an execution payload has arrived (Gloas fork)
192
210
  * Creates the FULL variant of a Gloas block when the payload becomes available
@@ -198,11 +216,13 @@ export interface IForkChoice {
198
216
  * @param blockRoot - The beacon block root for which the payload arrived
199
217
  * @param executionPayloadBlockHash - The block hash of the execution payload
200
218
  * @param executionPayloadNumber - The block number of the execution payload
219
+ * @param executionPayloadGasLimit - The gas limit of the execution payload
201
220
  */
202
221
  onExecutionPayload(
203
222
  blockRoot: RootHex,
204
223
  executionPayloadBlockHash: RootHex,
205
224
  executionPayloadNumber: number,
225
+ executionPayloadGasLimit: number,
206
226
  executionStatus: PayloadExecutionStatus,
207
227
  dataAvailabilityStatus: DataAvailabilityStatus
208
228
  ): void;
@@ -233,6 +253,15 @@ export interface IForkChoice {
233
253
  hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
234
254
  getSlotsPresent(windowStart: number): number;
235
255
  getPTCVotes(blockRootHex: RootHex): (boolean | null)[] | null;
256
+ /** Raw PTC vote tallies for the debug fork choice endpoint; `null` for pre-Gloas roots. */
257
+ getPTCVoteCounts(blockRootHex: RootHex): {
258
+ attesterCount: number;
259
+ payloadPresentCount: number;
260
+ dataAvailableCount: number;
261
+ } | null;
262
+ getPayloadTimelinessVotes(blockRootHex: RootHex): (boolean | null)[] | null;
263
+ getPayloadDataAvailabilityVotes(blockRootHex: RootHex): (boolean | null)[] | null;
264
+
236
265
  /**
237
266
  * Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
238
267
  */
@@ -242,6 +271,8 @@ export interface IForkChoice {
242
271
  getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null;
243
272
  getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null;
244
273
  shouldExtendPayload(blockRoot: RootHex): boolean;
274
+ /** Spec: should_build_on_full(store, head) */
275
+ shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean;
245
276
  getFinalizedBlock(): ProtoBlock;
246
277
  getJustifiedBlock(): ProtoBlock;
247
278
  getFinalizedCheckpointSlot(): Slot;
@@ -1,5 +1,6 @@
1
1
  import {ZERO_HASH_HEX} from "@lodestar/params";
2
2
  import {Root, RootHex} from "@lodestar/types";
3
+ import {fromHex} from "@lodestar/utils";
3
4
  import {IForkChoice} from "./interface.js";
4
5
 
5
6
  /**
@@ -7,21 +8,28 @@ import {IForkChoice} from "./interface.js";
7
8
  * that is safe from re-orgs. Normally this block is pretty close to the head of canonical
8
9
  * chain which makes it valuable to expose a safe block to users.
9
10
  *
10
- * https://github.com/ethereum/consensus-specs/blob/v1.6.0/fork_choice/safe-block.md#get_safe_beacon_block_root
11
+ * @deprecated The merged fast-confirmation spec only defines `get_safe_execution_block_hash`.
11
12
  */
12
13
  export function getSafeBeaconBlockRoot(fc: IForkChoice): Root {
14
+ const confirmedRoot = fc.getConfirmedRoot();
15
+ if (confirmedRoot && fc.hasBlockHex(confirmedRoot)) {
16
+ return fromHex(confirmedRoot);
17
+ }
13
18
  return fc.getJustifiedCheckpoint().root;
14
19
  }
15
20
 
16
21
  /**
17
22
  * Get execution payload hash for the safe block
18
- * This function assumes that safe block is post Bellatrix and function should not be called otherwise.
19
23
  *
20
- * As our existing usage is aligned with above condition so not adding fork-check inside this function
21
- *
22
- *
23
- * https://github.com/ethereum/consensus-specs/blob/v1.6.0/fork_choice/safe-block.md#get_safe_execution_block_hash
24
+ * https://github.com/ethereum/consensus-specs/blob/master/fork_choice/safe-block.md#get_safe_execution_block_hash
24
25
  */
25
26
  export function getSafeExecutionBlockHash(forkChoice: IForkChoice): RootHex {
26
- return forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
27
+ const confirmedRoot = forkChoice.getConfirmedRoot();
28
+ if (confirmedRoot) {
29
+ const confirmedBlock = forkChoice.getBlockHexDefaultStatus(confirmedRoot);
30
+ if (confirmedBlock?.executionPayloadBlockHash) {
31
+ return confirmedBlock.executionPayloadBlockHash;
32
+ }
33
+ }
34
+ return ZERO_HASH_HEX;
27
35
  }
@@ -1,6 +1,7 @@
1
1
  import {EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
2
2
  import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
3
3
  import {toRootHex} from "@lodestar/utils";
4
+ import {ForkChoiceStateGetter, IFastConfirmationStore} from "./fastConfirmation/types.ts";
4
5
  import {CheckpointWithBalance, CheckpointWithTotalBalance} from "./interface.js";
5
6
 
6
7
  /**
@@ -35,7 +36,7 @@ export type JustifiedBalancesGetter = (
35
36
  * - The actual block DAG in `ProtoArray`.
36
37
  * - `time` is represented using `Slot` instead of UNIX epoch `u64`.
37
38
  */
38
- export interface IForkChoiceStore {
39
+ export interface IForkChoiceStore extends IFastConfirmationStore {
39
40
  currentSlot: Slot;
40
41
  get justified(): CheckpointWithTotalBalance;
41
42
  set justified(justified: CheckpointWithBalance);
@@ -44,6 +45,7 @@ export interface IForkChoiceStore {
44
45
  unrealizedFinalizedCheckpoint: CheckpointWithHex;
45
46
  justifiedBalancesGetter: JustifiedBalancesGetter;
46
47
  equivocatingIndices: Set<ValidatorIndex>;
48
+ notifyFastConfirmation?(data: {block: RootHex; slot: Slot; currentSlot: Slot}): void;
47
49
  }
48
50
 
49
51
  /**
@@ -58,19 +60,36 @@ export class ForkChoiceStore implements IForkChoiceStore {
58
60
  justifiedBalancesGetter: JustifiedBalancesGetter;
59
61
  currentSlot: Slot;
60
62
 
63
+ // Fast Confirmation Rule spec fields
64
+ confirmedRoot: RootHex;
65
+ previousEpochObservedJustifiedCheckpoint: CheckpointWithHex;
66
+ currentEpochObservedJustifiedCheckpoint: CheckpointWithHex;
67
+ previousEpochGreatestUnrealizedCheckpoint: CheckpointWithHex;
68
+ previousSlotHead: RootHex;
69
+ currentSlotHead: RootHex;
70
+
71
+ // Fast Confirmation Rule internal fields
72
+ previousEpochObservedJustifiedBalances: JustifiedBalances;
73
+ currentEpochObservedJustifiedBalances: JustifiedBalances;
74
+ previousEpochGreatestUnrealizedBalances: JustifiedBalances;
75
+ stateGetter: ForkChoiceStateGetter;
76
+
61
77
  constructor(
62
78
  currentSlot: Slot,
63
79
  justifiedCheckpoint: phase0.Checkpoint,
64
80
  finalizedCheckpoint: phase0.Checkpoint,
65
81
  justifiedBalances: EffectiveBalanceIncrements,
66
82
  justifiedBalancesGetter: JustifiedBalancesGetter,
83
+ stateGetter: ForkChoiceStateGetter,
67
84
  private readonly events?: {
68
85
  onJustified: (cp: CheckpointWithHex) => void;
69
86
  onFinalized: (cp: CheckpointWithHex) => void;
87
+ onFastConfirmation?: (data: {block: RootHex; slot: Slot; currentSlot: Slot}) => void;
70
88
  }
71
89
  ) {
72
90
  this.justifiedBalancesGetter = justifiedBalancesGetter;
73
91
  this.currentSlot = currentSlot;
92
+ this.stateGetter = stateGetter;
74
93
  const justified = {
75
94
  checkpoint: toCheckpointWithHex(justifiedCheckpoint),
76
95
  balances: justifiedBalances,
@@ -80,6 +99,22 @@ export class ForkChoiceStore implements IForkChoiceStore {
80
99
  this.unrealizedJustified = justified;
81
100
  this._finalizedCheckpoint = toCheckpointWithHex(finalizedCheckpoint);
82
101
  this.unrealizedFinalizedCheckpoint = this._finalizedCheckpoint;
102
+
103
+ // Initialize Fast Confirmation fields conservatively from finalized, matching
104
+ // the spec's get_fast_confirmation_store() behavior.
105
+ const finalizedCheckpointWithHex = toCheckpointWithHex(finalizedCheckpoint);
106
+ const finalizedState = stateGetter({checkpoint: finalizedCheckpointWithHex});
107
+ const finalizedBalances = finalizedState?.effectiveBalanceIncrements ?? justifiedBalances;
108
+ const anchorRoot = finalizedCheckpointWithHex.rootHex;
109
+ this.previousEpochObservedJustifiedCheckpoint = finalizedCheckpointWithHex;
110
+ this.currentEpochObservedJustifiedCheckpoint = finalizedCheckpointWithHex;
111
+ this.previousEpochGreatestUnrealizedCheckpoint = finalizedCheckpointWithHex;
112
+ this.confirmedRoot = anchorRoot;
113
+ this.previousEpochObservedJustifiedBalances = finalizedBalances;
114
+ this.currentEpochObservedJustifiedBalances = finalizedBalances;
115
+ this.previousEpochGreatestUnrealizedBalances = finalizedBalances;
116
+ this.previousSlotHead = anchorRoot;
117
+ this.currentSlotHead = anchorRoot;
83
118
  }
84
119
 
85
120
  get justified(): CheckpointWithTotalBalance {
@@ -98,6 +133,15 @@ export class ForkChoiceStore implements IForkChoiceStore {
98
133
  this._finalizedCheckpoint = cp;
99
134
  this.events?.onFinalized(cp);
100
135
  }
136
+
137
+ /**
138
+ * Notify subscribers that the Fast Confirmation Rule executed and produced
139
+ * `data.block` at `data.slot`. Fires once per FCR execution, even when
140
+ * `confirmedRoot` did not change from the prior slot.
141
+ */
142
+ notifyFastConfirmation(data: {block: RootHex; slot: Slot; currentSlot: Slot}): void {
143
+ this.events?.onFastConfirmation?.(data);
144
+ }
101
145
  }
102
146
 
103
147
  export function toCheckpointWithHex(checkpoint: phase0.Checkpoint): CheckpointWithHex {
package/src/index.ts CHANGED
@@ -6,6 +6,17 @@ export {
6
6
  type InvalidBlock,
7
7
  InvalidBlockCode,
8
8
  } from "./forkChoice/errors.js";
9
+ export {
10
+ type FastConfirmationBalanceSource,
11
+ type FastConfirmationContext,
12
+ type FastConfirmationMetrics,
13
+ type FastConfirmationResult,
14
+ FastConfirmationRule,
15
+ type ForkChoiceStateGetter,
16
+ type IFastConfirmationRule,
17
+ type IFastConfirmationStore,
18
+ getFastConfirmationMetrics,
19
+ } from "./forkChoice/fastConfirmation/fastConfirmationRule.ts";
9
20
  export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
10
21
  export {
11
22
  type AncestorResult,
package/src/metrics.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import {MetricsRegisterExtra} from "@lodestar/utils";
2
+ import {FastConfirmationMetrics, getFastConfirmationMetrics} from "./forkChoice/fastConfirmation/metrics.ts";
2
3
  import {UpdateHeadOpt} from "./forkChoice/forkChoice.js";
3
4
  import {NotReorgedReason} from "./forkChoice/interface.js";
4
5
 
5
- export type ForkChoiceMetrics = ReturnType<typeof getForkChoiceMetrics>;
6
+ export type ForkChoiceMetrics = ReturnType<typeof getForkChoiceMetrics> & FastConfirmationMetrics;
6
7
 
7
8
  export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
8
9
  return {
10
+ ...getFastConfirmationMetrics(register),
9
11
  forkChoice: {
10
12
  findHead: register.histogram<{caller: string}>({
11
13
  name: "beacon_fork_choice_find_head_seconds",
@@ -87,6 +87,14 @@ export type BlockExtraMeta =
87
87
  // - payload block hash for FULL variant
88
88
  executionPayloadBlockHash: RootHex;
89
89
  executionPayloadNumber: UintNum64;
90
+ // Gas limit of the executed payload identified by executionPayloadBlockHash. Set on
91
+ // pre-Gloas blocks (from block.body.executionPayload.gasLimit) and on Gloas variants:
92
+ // - PENDING/EMPTY: inherited from the parent payload that the bid commits to extend
93
+ // (matches executionPayloadBlockHash, which also points to that parent payload)
94
+ // - FULL: the actual delivered payload's gasLimit (set in onExecutionPayload)
95
+ // Consumers (e.g. Gloas bid gas-limit validation) can read this without re-deriving from
96
+ // state.
97
+ executionPayloadGasLimit: UintNum64;
90
98
  executionStatus: Exclude<ExecutionStatus, ExecutionStatus.PreMerge>;
91
99
  dataAvailabilityStatus: DataAvailabilityStatus;
92
100
  }