@lodestar/fork-choice 1.44.0-dev.8793c24169 → 1.44.0-dev.9666fc891a

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 (69) 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 +129 -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 +21 -0
  14. package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -0
  15. package/lib/forkChoice/fastConfirmation/metrics.js +42 -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 +91 -0
  20. package/lib/forkChoice/fastConfirmation/rules.js.map +1 -0
  21. package/lib/forkChoice/fastConfirmation/types.d.ts +101 -0
  22. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -0
  23. package/lib/forkChoice/fastConfirmation/types.js +12 -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 +681 -0
  28. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -0
  29. package/lib/forkChoice/forkChoice.d.ts +18 -0
  30. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  31. package/lib/forkChoice/forkChoice.js +102 -5
  32. package/lib/forkChoice/forkChoice.js.map +1 -1
  33. package/lib/forkChoice/interface.d.ts +14 -0
  34. package/lib/forkChoice/interface.d.ts.map +1 -1
  35. package/lib/forkChoice/safeBlocks.d.ts +2 -6
  36. package/lib/forkChoice/safeBlocks.d.ts.map +1 -1
  37. package/lib/forkChoice/safeBlocks.js +15 -7
  38. package/lib/forkChoice/safeBlocks.js.map +1 -1
  39. package/lib/forkChoice/store.d.ts +13 -2
  40. package/lib/forkChoice/store.d.ts.map +1 -1
  41. package/lib/forkChoice/store.js +29 -1
  42. package/lib/forkChoice/store.js.map +1 -1
  43. package/lib/index.d.ts +1 -0
  44. package/lib/index.d.ts.map +1 -1
  45. package/lib/index.js +1 -0
  46. package/lib/index.js.map +1 -1
  47. package/lib/metrics.d.ts +12 -1
  48. package/lib/metrics.d.ts.map +1 -1
  49. package/lib/metrics.js +2 -0
  50. package/lib/metrics.js.map +1 -1
  51. package/lib/protoArray/protoArray.d.ts +23 -1
  52. package/lib/protoArray/protoArray.d.ts.map +1 -1
  53. package/lib/protoArray/protoArray.js +50 -4
  54. package/lib/protoArray/protoArray.js.map +1 -1
  55. package/package.json +7 -7
  56. package/src/forkChoice/fastConfirmation/data.ts +43 -0
  57. package/src/forkChoice/fastConfirmation/fastConfirmationRule.ts +159 -0
  58. package/src/forkChoice/fastConfirmation/index.ts +3 -0
  59. package/src/forkChoice/fastConfirmation/metrics.ts +44 -0
  60. package/src/forkChoice/fastConfirmation/rules.ts +124 -0
  61. package/src/forkChoice/fastConfirmation/types.ts +111 -0
  62. package/src/forkChoice/fastConfirmation/utils.ts +968 -0
  63. package/src/forkChoice/forkChoice.ts +129 -6
  64. package/src/forkChoice/interface.ts +15 -0
  65. package/src/forkChoice/safeBlocks.ts +15 -7
  66. package/src/forkChoice/store.ts +34 -1
  67. package/src/index.ts +11 -0
  68. package/src/metrics.ts +3 -1
  69. package/src/protoArray/protoArray.ts +60 -4
@@ -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`.
@@ -994,12 +1021,12 @@ export class ForkChoice implements IForkChoice {
994
1021
  const previousSlot = this.fcStore.currentSlot;
995
1022
  // Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
996
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();
997
1029
  }
998
-
999
- this.queuedAttestationsPreviousSlot = 0;
1000
- // Process any attestations that might now be eligible.
1001
- this.processAttestationQueue();
1002
- this.validatedAttestationDatas = new Set();
1003
1030
  }
1004
1031
 
1005
1032
  getTime(): Slot {
@@ -1068,6 +1095,30 @@ export class ForkChoice implements IForkChoice {
1068
1095
  return votes.toBoolArray().map((v) => v ?? null);
1069
1096
  }
1070
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
+
1071
1122
  /**
1072
1123
  * Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
1073
1124
  */
@@ -1893,6 +1944,78 @@ export class ForkChoice implements IForkChoice {
1893
1944
 
1894
1945
  return {prelimProposerHead};
1895
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
+ } catch (err) {
1963
+ this.logger?.debug(
1964
+ "Fast confirmation failed",
1965
+ {slot: this.fcStore.currentSlot, head: this.head.blockRoot, confirmedRoot: this.fcStore.confirmedRoot},
1966
+ err as Error
1967
+ );
1968
+ }
1969
+ });
1970
+ }
1971
+
1972
+ private createFastConfirmationContext(): FastConfirmationContext {
1973
+ const confirmationByzantineThreshold = this.config.CONFIRMATION_BYZANTINE_THRESHOLD;
1974
+ if (!confirmationByzantineThreshold) {
1975
+ throw new Error("CONFIRMATION_BYZANTINE_THRESHOLD must be set to use fast confirmation");
1976
+ }
1977
+
1978
+ return {
1979
+ config: {
1980
+ CONFIRMATION_BYZANTINE_THRESHOLD: confirmationByzantineThreshold,
1981
+ PROPOSER_SCORE_BOOST: this.config.PROPOSER_SCORE_BOOST,
1982
+ },
1983
+ getCurrentSlot: () => this.fcStore.currentSlot,
1984
+ getHead: () => this.head,
1985
+ getBlock: (root: RootHex) => this.getBlockHexDefaultStatus(root),
1986
+ getAncestor: (root: RootHex, slot: Slot) => this.getAncestor(root, slot).blockRoot,
1987
+ isDescendant: (ancestor: RootHex, descendant: RootHex) => {
1988
+ const ancestorStatus = this.protoArray.getDefaultVariant(ancestor);
1989
+ const descendantStatus = this.protoArray.getDefaultVariant(descendant);
1990
+ if (ancestorStatus === undefined || descendantStatus === undefined) return false;
1991
+ return this.isDescendant(ancestor, ancestorStatus, descendant, descendantStatus);
1992
+ },
1993
+ getLatestMessage: (validatorIndex: ValidatorIndex) => {
1994
+ const nextIndex = this.voteNextIndices[validatorIndex];
1995
+ if (nextIndex === undefined || nextIndex === NULL_VOTE_INDEX) {
1996
+ return null;
1997
+ }
1998
+ const node = this.protoArray.nodes[nextIndex];
1999
+ if (!node) return null;
2000
+ return {root: node.blockRoot, epoch: computeEpochAtSlot(this.voteNextSlots[validatorIndex])};
2001
+ },
2002
+ getUnrealizedJustified: () => ({
2003
+ checkpoint: this.fcStore.unrealizedJustified.checkpoint,
2004
+ balances: this.fcStore.unrealizedJustified.balances,
2005
+ }),
2006
+ getFinalizedCheckpoint: () => this.fcStore.finalizedCheckpoint,
2007
+ getEquivocatingIndices: () => this.fcStore.equivocatingIndices,
2008
+ getTrackedVotesCount: () => {
2009
+ let count = 0;
2010
+ for (let i = 0; i < this.voteNextIndices.length; i++) {
2011
+ if (this.voteNextIndices[i] !== NULL_VOTE_INDEX) {
2012
+ count++;
2013
+ }
2014
+ }
2015
+ return count;
2016
+ },
2017
+ };
2018
+ }
1896
2019
  }
1897
2020
 
1898
2021
  // Approximate https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#calculate_committee_fraction
@@ -107,6 +107,8 @@ export interface IForkChoice {
107
107
  */
108
108
  getHeadRoot(): RootHex;
109
109
  getHead(): ProtoBlock;
110
+ getConfirmedRoot(): RootHex;
111
+ getConfirmedBlock(): ProtoBlock | null;
110
112
  updateAndGetHead(mode: UpdateAndGetHeadOpt): {
111
113
  head: ProtoBlock;
112
114
  isHeadTimely?: boolean;
@@ -132,6 +134,10 @@ export interface IForkChoice {
132
134
  getAllNodes(): ProtoNode[];
133
135
  getFinalizedCheckpoint(): CheckpointWithHex;
134
136
  getJustifiedCheckpoint(): CheckpointWithHex;
137
+ getUnrealizedJustifiedCheckpoint(): CheckpointWithHex;
138
+ getUnrealizedFinalizedCheckpoint(): CheckpointWithHex;
139
+ getProposerBoostRoot(): RootHex;
140
+ getPreviousProposerBoostRoot(): RootHex;
135
141
  /**
136
142
  * Add `block` to the fork choice DAG.
137
143
  *
@@ -247,6 +253,15 @@ export interface IForkChoice {
247
253
  hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
248
254
  getSlotsPresent(windowStart: number): number;
249
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
+
250
265
  /**
251
266
  * Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
252
267
  */
@@ -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);
@@ -58,12 +59,27 @@ export class ForkChoiceStore implements IForkChoiceStore {
58
59
  justifiedBalancesGetter: JustifiedBalancesGetter;
59
60
  currentSlot: Slot;
60
61
 
62
+ // Fast Confirmation Rule spec fields
63
+ confirmedRoot: RootHex;
64
+ previousEpochObservedJustifiedCheckpoint: CheckpointWithHex;
65
+ currentEpochObservedJustifiedCheckpoint: CheckpointWithHex;
66
+ previousEpochGreatestUnrealizedCheckpoint: CheckpointWithHex;
67
+ previousSlotHead: RootHex;
68
+ currentSlotHead: RootHex;
69
+
70
+ // Fast Confirmation Rule internal fields
71
+ previousEpochObservedJustifiedBalances: JustifiedBalances;
72
+ currentEpochObservedJustifiedBalances: JustifiedBalances;
73
+ previousEpochGreatestUnrealizedBalances: JustifiedBalances;
74
+ stateGetter: ForkChoiceStateGetter;
75
+
61
76
  constructor(
62
77
  currentSlot: Slot,
63
78
  justifiedCheckpoint: phase0.Checkpoint,
64
79
  finalizedCheckpoint: phase0.Checkpoint,
65
80
  justifiedBalances: EffectiveBalanceIncrements,
66
81
  justifiedBalancesGetter: JustifiedBalancesGetter,
82
+ stateGetter: ForkChoiceStateGetter,
67
83
  private readonly events?: {
68
84
  onJustified: (cp: CheckpointWithHex) => void;
69
85
  onFinalized: (cp: CheckpointWithHex) => void;
@@ -71,6 +87,7 @@ export class ForkChoiceStore implements IForkChoiceStore {
71
87
  ) {
72
88
  this.justifiedBalancesGetter = justifiedBalancesGetter;
73
89
  this.currentSlot = currentSlot;
90
+ this.stateGetter = stateGetter;
74
91
  const justified = {
75
92
  checkpoint: toCheckpointWithHex(justifiedCheckpoint),
76
93
  balances: justifiedBalances,
@@ -80,6 +97,22 @@ export class ForkChoiceStore implements IForkChoiceStore {
80
97
  this.unrealizedJustified = justified;
81
98
  this._finalizedCheckpoint = toCheckpointWithHex(finalizedCheckpoint);
82
99
  this.unrealizedFinalizedCheckpoint = this._finalizedCheckpoint;
100
+
101
+ // Initialize Fast Confirmation fields conservatively from finalized, matching
102
+ // the spec's get_fast_confirmation_store() behavior.
103
+ const finalizedCheckpointWithHex = toCheckpointWithHex(finalizedCheckpoint);
104
+ const finalizedState = stateGetter({checkpoint: finalizedCheckpointWithHex});
105
+ const finalizedBalances = finalizedState?.effectiveBalanceIncrements ?? justifiedBalances;
106
+ const anchorRoot = finalizedCheckpointWithHex.rootHex;
107
+ this.previousEpochObservedJustifiedCheckpoint = finalizedCheckpointWithHex;
108
+ this.currentEpochObservedJustifiedCheckpoint = finalizedCheckpointWithHex;
109
+ this.previousEpochGreatestUnrealizedCheckpoint = finalizedCheckpointWithHex;
110
+ this.confirmedRoot = anchorRoot;
111
+ this.previousEpochObservedJustifiedBalances = finalizedBalances;
112
+ this.currentEpochObservedJustifiedBalances = finalizedBalances;
113
+ this.previousEpochGreatestUnrealizedBalances = finalizedBalances;
114
+ this.previousSlotHead = anchorRoot;
115
+ this.currentSlotHead = anchorRoot;
83
116
  }
84
117
 
85
118
  get justified(): CheckpointWithTotalBalance {
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",
@@ -718,6 +718,58 @@ export class ProtoArray {
718
718
  return votes;
719
719
  }
720
720
 
721
+ /**
722
+ * Raw PTC vote tallies for a block root, for the debug fork choice endpoint.
723
+ * Returns `null` for pre-Gloas (or pruned) roots, which have no vote maps.
724
+ */
725
+ getPTCVoteCounts(blockRootHex: RootHex): {
726
+ attesterCount: number;
727
+ payloadPresentCount: number;
728
+ dataAvailableCount: number;
729
+ } | null {
730
+ const attended = this.ptcAttested.get(blockRootHex);
731
+ const timelinessVotes = this.payloadTimelinessVotes.get(blockRootHex);
732
+ const daVotes = this.payloadDataAvailabilityVotes.get(blockRootHex);
733
+ // The three maps share a lifecycle (set together in onBlock, deleted together on prune)
734
+ if (attended === undefined || timelinessVotes === undefined || daVotes === undefined) {
735
+ return null;
736
+ }
737
+ return {
738
+ attesterCount: bitCount(attended.uint8Array),
739
+ payloadPresentCount: bitCount(timelinessVotes.uint8Array),
740
+ dataAvailableCount: bitCount(daVotes.uint8Array),
741
+ };
742
+ }
743
+
744
+ getPreviousProposerBoostRoot(): RootHex {
745
+ return this.previousProposerBoost?.root ?? HEX_ZERO_HASH;
746
+ }
747
+
748
+ /**
749
+ * Timeliness votes per PTC position, `null` where the member has not attested.
750
+ * Returns `null` if the block is unknown or not a Gloas block.
751
+ */
752
+ getPayloadTimelinessVotes(blockRootHex: RootHex): (boolean | null)[] | null {
753
+ return this.toAttendanceAwareVotes(this.payloadTimelinessVotes.get(blockRootHex), blockRootHex);
754
+ }
755
+
756
+ /**
757
+ * Data-availability votes per PTC position, `null` where the member has not attested.
758
+ * Returns `null` if the block is unknown or not a Gloas block.
759
+ */
760
+ getPayloadDataAvailabilityVotes(blockRootHex: RootHex): (boolean | null)[] | null {
761
+ return this.toAttendanceAwareVotes(this.payloadDataAvailabilityVotes.get(blockRootHex), blockRootHex);
762
+ }
763
+
764
+ private toAttendanceAwareVotes(votes: BitArray | undefined, blockRootHex: RootHex): (boolean | null)[] | null {
765
+ const attended = this.ptcAttested.get(blockRootHex);
766
+ if (votes === undefined || attended === undefined) {
767
+ return null;
768
+ }
769
+
770
+ return Array.from({length: PTC_SIZE}, (_, i) => (attended.get(i) ? votes.get(i) : null));
771
+ }
772
+
721
773
  /**
722
774
  * Spec: payload_timeliness(store, root, timely=True)
723
775
  */
@@ -766,7 +818,8 @@ export class ProtoArray {
766
818
  * Spec: should_build_on_full(store, head)
767
819
  *
768
820
  * The proposer is forced to build on the EMPTY variant (effectively reorging)
769
- * when the PTC majority voted that the blob data is not available.
821
+ * when the PTC majority voted that the blob data is not available or that the
822
+ * payload was not timely.
770
823
  */
771
824
  shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
772
825
  if (head.payloadStatus === PayloadStatus.PENDING) {
@@ -774,11 +827,14 @@ export class ProtoArray {
774
827
  }
775
828
  if (head.payloadStatus === PayloadStatus.EMPTY) return false;
776
829
 
777
- // The PTC data availability view is only consulted for a head from the previous slot.
778
- // For an earlier head the empty/full variant has already been resolved by weight in getHead.
830
+ // The PTC data availability and timeliness views are only consulted for a head from the
831
+ // previous slot. For an earlier head the empty/full variant has already been resolved by
832
+ // weight in getHead.
779
833
  if (head.slot + 1 !== slot) return true;
780
834
 
781
- return !this.isPayloadDataNotAvailable(head.blockRoot);
835
+ if (this.isPayloadDataNotAvailable(head.blockRoot)) return false;
836
+
837
+ return !this.isPayloadNotTimely(head.blockRoot);
782
838
  }
783
839
 
784
840
  /**