@lodestar/fork-choice 1.50.0-dev.586c4d5d51 → 1.50.0-dev.5a1db4e01c

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 (43) hide show
  1. package/lib/forkChoice/fastConfirmation/types.d.ts +3 -1
  2. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
  3. package/lib/forkChoice/fastConfirmation/utils.d.ts +2 -2
  4. package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
  5. package/lib/forkChoice/fastConfirmation/utils.js +8 -3
  6. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
  7. package/lib/forkChoice/forkChoice.d.ts +2 -1
  8. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  9. package/lib/forkChoice/forkChoice.js +37 -3
  10. package/lib/forkChoice/forkChoice.js.map +1 -1
  11. package/lib/forkChoice/interface.d.ts +9 -1
  12. package/lib/forkChoice/interface.d.ts.map +1 -1
  13. package/lib/forkChoice/interface.js.map +1 -1
  14. package/lib/forkChoice/store.d.ts +4 -1
  15. package/lib/forkChoice/store.d.ts.map +1 -1
  16. package/lib/forkChoice/store.js +3 -0
  17. package/lib/forkChoice/store.js.map +1 -1
  18. package/lib/index.d.ts +3 -3
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +1 -1
  21. package/lib/index.js.map +1 -1
  22. package/lib/metrics.d.ts +3 -0
  23. package/lib/metrics.d.ts.map +1 -1
  24. package/lib/metrics.js +5 -0
  25. package/lib/metrics.js.map +1 -1
  26. package/lib/protoArray/interface.d.ts +15 -0
  27. package/lib/protoArray/interface.d.ts.map +1 -1
  28. package/lib/protoArray/interface.js +13 -0
  29. package/lib/protoArray/interface.js.map +1 -1
  30. package/lib/protoArray/protoArray.d.ts +18 -2
  31. package/lib/protoArray/protoArray.d.ts.map +1 -1
  32. package/lib/protoArray/protoArray.js +57 -3
  33. package/lib/protoArray/protoArray.js.map +1 -1
  34. package/package.json +8 -8
  35. package/src/forkChoice/fastConfirmation/types.ts +3 -2
  36. package/src/forkChoice/fastConfirmation/utils.ts +12 -5
  37. package/src/forkChoice/forkChoice.ts +38 -2
  38. package/src/forkChoice/interface.ts +6 -0
  39. package/src/forkChoice/store.ts +7 -1
  40. package/src/index.ts +3 -1
  41. package/src/metrics.ts +5 -0
  42. package/src/protoArray/interface.ts +21 -0
  43. package/src/protoArray/protoArray.ts +64 -2
@@ -7,6 +7,7 @@ import {
7
7
  PayloadStatus,
8
8
  ProtoBlock,
9
9
  ProtoNode,
10
+ PtcQuorum,
10
11
  } from "../protoArray/interface.js";
11
12
  import {IFastConfirmationSpecStore} from "./fastConfirmation/types.js";
12
13
  import {UpdateAndGetHeadOpt} from "./forkChoice.js";
@@ -31,6 +32,9 @@ export type CheckpointWithTotalBalance = CheckpointWithBalance & {
31
32
  totalBalance: number;
32
33
  };
33
34
 
35
+ /** Emitted when the PTC verdict on a block is decided or flips, see getPtcVerdict */
36
+ export type PtcQuorumEvent = PtcQuorum & {blockRoot: RootHex; slot: Slot; verdict: boolean};
37
+
34
38
  export enum EpochDifference {
35
39
  current = 0,
36
40
  previous = 1,
@@ -259,6 +263,8 @@ export interface IForkChoice {
259
263
  payloadPresentCount: number;
260
264
  dataAvailableCount: number;
261
265
  } | null;
266
+ /** Current PTC majority per vote field; `null` for pre-Gloas roots */
267
+ getPtcQuorum(blockRootHex: RootHex): PtcQuorum | null;
262
268
  getPayloadTimelinessVotes(blockRootHex: RootHex): (boolean | null)[] | null;
263
269
  getPayloadDataAvailabilityVotes(blockRootHex: RootHex): (boolean | null)[] | null;
264
270
 
@@ -2,7 +2,7 @@ import {EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-tran
2
2
  import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
3
3
  import {toRootHex} from "@lodestar/utils";
4
4
  import {ForkChoiceStateGetter, IFastConfirmationStore} from "./fastConfirmation/types.js";
5
- import {CheckpointWithBalance, CheckpointWithTotalBalance} from "./interface.js";
5
+ import {CheckpointWithBalance, CheckpointWithTotalBalance, PtcQuorumEvent} from "./interface.js";
6
6
 
7
7
  /**
8
8
  * Stores checkpoints in a hybrid format:
@@ -46,6 +46,7 @@ export interface IForkChoiceStore extends IFastConfirmationStore {
46
46
  justifiedBalancesGetter: JustifiedBalancesGetter;
47
47
  equivocatingIndices: Set<ValidatorIndex>;
48
48
  notifyFastConfirmation?(data: {block: RootHex; slot: Slot; currentSlot: Slot}): void;
49
+ notifyPtcQuorum?(data: PtcQuorumEvent): void;
49
50
  }
50
51
 
51
52
  /**
@@ -85,6 +86,7 @@ export class ForkChoiceStore implements IForkChoiceStore {
85
86
  onJustified: (cp: CheckpointWithHex) => void;
86
87
  onFinalized: (cp: CheckpointWithHex) => void;
87
88
  onFastConfirmation?: (data: {block: RootHex; slot: Slot; currentSlot: Slot}) => void;
89
+ onPtcQuorum?: (data: PtcQuorumEvent) => void;
88
90
  }
89
91
  ) {
90
92
  this.justifiedBalancesGetter = justifiedBalancesGetter;
@@ -142,6 +144,10 @@ export class ForkChoiceStore implements IForkChoiceStore {
142
144
  notifyFastConfirmation(data: {block: RootHex; slot: Slot; currentSlot: Slot}): void {
143
145
  this.events?.onFastConfirmation?.(data);
144
146
  }
147
+
148
+ notifyPtcQuorum(data: PtcQuorumEvent): void {
149
+ this.events?.onPtcQuorum?.(data);
150
+ }
145
151
  }
146
152
 
147
153
  export function toCheckpointWithHex(checkpoint: phase0.Checkpoint): CheckpointWithHex {
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  EpochDifference,
27
27
  type IForkChoice,
28
28
  NotReorgedReason,
29
+ type PtcQuorumEvent,
29
30
  } from "./forkChoice/interface.js";
30
31
  export * from "./forkChoice/safeBlocks.js";
31
32
  export {
@@ -43,6 +44,7 @@ export type {
43
44
  PayloadExecutionStatus,
44
45
  ProtoBlock,
45
46
  ProtoNode,
47
+ PtcQuorum,
46
48
  } from "./protoArray/interface.js";
47
- export {ExecutionStatus, PayloadStatus, isGloasBlock} from "./protoArray/interface.js";
49
+ export {ExecutionStatus, PayloadStatus, getPtcVerdict, isGloasBlock} from "./protoArray/interface.js";
48
50
  export {ProtoArray} from "./protoArray/protoArray.js";
package/src/metrics.ts CHANGED
@@ -68,6 +68,11 @@ export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
68
68
  help: "Reason why the current head is not re-orged out",
69
69
  labelNames: ["reason"],
70
70
  }),
71
+ ptcQuorum: register.counter<{verdict: "true" | "false"}>({
72
+ name: "beacon_fork_choice_ptc_quorum_total",
73
+ help: "Count of decided PTC verdicts, true once majorities voted present and available, false once a majority voted either against",
74
+ labelNames: ["verdict"],
75
+ }),
71
76
  computeDeltas: {
72
77
  duration: register.histogram({
73
78
  name: "beacon_fork_choice_compute_deltas_seconds",
@@ -180,3 +180,24 @@ export type ProtoNode = ProtoBlock & {
180
180
  bestChild?: number;
181
181
  bestDescendant?: number;
182
182
  };
183
+
184
+ /**
185
+ * PTC majority per PayloadAttestationData field: `true` or `false` once more than PTC_SIZE // 2
186
+ * members voted that way, `null` while neither outcome has a majority.
187
+ */
188
+ export type PtcQuorum = {
189
+ payloadPresent: boolean | null;
190
+ blobDataAvailable: boolean | null;
191
+ };
192
+
193
+ /**
194
+ * The PTC verdict fork choice acts on: `false` once either field has a majority against the
195
+ * payload, `true` once both have a majority for it, `null` while undecided. A single majority
196
+ * against is final on its own because should_extend_payload needs both in favour and
197
+ * should_build_on_full moves the proposer onto EMPTY on either.
198
+ */
199
+ export function getPtcVerdict({payloadPresent, blobDataAvailable}: PtcQuorum): boolean | null {
200
+ if (payloadPresent === false || blobDataAvailable === false) return false;
201
+ if (payloadPresent === true && blobDataAvailable === true) return true;
202
+ return null;
203
+ }
@@ -13,6 +13,7 @@ import {
13
13
  PayloadStatus,
14
14
  ProtoBlock,
15
15
  ProtoNode,
16
+ PtcQuorum,
16
17
  isGloasBlock,
17
18
  } from "./interface.js";
18
19
 
@@ -52,6 +53,12 @@ export function countNoVotes(attended: BitArray, yes: BitArray): number {
52
53
  return count;
53
54
  }
54
55
 
56
+ function majorityVote(attended: BitArray, yes: BitArray, threshold: number): boolean | null {
57
+ if (bitCount(yes.uint8Array) > threshold) return true;
58
+ if (countNoVotes(attended, yes) > threshold) return false;
59
+ return null;
60
+ }
61
+
55
62
  export const DEFAULT_PRUNE_THRESHOLD = 0;
56
63
  type ProposerBoost = {root: RootHex; score: bigint};
57
64
 
@@ -90,6 +97,8 @@ export class ProtoArray {
90
97
  lvhError?: LVHExecError;
91
98
 
92
99
  private previousProposerBoost: ProposerBoost | null = null;
100
+ /** Root read by shouldExtendPayload(), set even while the boost weight is withheld */
101
+ private proposerBoostRoot: RootHex | null = null;
93
102
 
94
103
  /**
95
104
  * PTC (Payload Timeliness Committee) votes per block as bitvectors
@@ -357,6 +366,7 @@ export class ProtoArray {
357
366
  applyScoreChanges({
358
367
  attestationDeltas,
359
368
  proposerBoost,
369
+ proposerBoostRoot,
360
370
  justifiedEpoch,
361
371
  justifiedRoot,
362
372
  finalizedEpoch,
@@ -365,6 +375,8 @@ export class ProtoArray {
365
375
  }: {
366
376
  attestationDeltas: number[];
367
377
  proposerBoost: ProposerBoost | null;
378
+ /** Defaults to the boosted root. Pass it explicitly when the boost weight is withheld */
379
+ proposerBoostRoot?: RootHex | null;
368
380
  justifiedEpoch: Epoch;
369
381
  justifiedRoot: RootHex;
370
382
  finalizedEpoch: Epoch;
@@ -463,7 +475,9 @@ export class ProtoArray {
463
475
  // We _must_ perform these functions separate from the weight-updating loop above to ensure
464
476
  // that we have a fully coherent set of weights before updating parent
465
477
  // best-child/descendant.
466
- const proposerBoostRoot = proposerBoost?.root ?? null;
478
+ // should_extend_payload reads store.proposer_boost_root even when should_apply_proposer_boost
479
+ // withholds the boost weight, so the tiebreak root is tracked apart from the applied boost
480
+ this.proposerBoostRoot = proposerBoostRoot ?? proposerBoost?.root ?? null;
467
481
  for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
468
482
  const node = this.nodes[nodeIndex];
469
483
  if (node === undefined) {
@@ -476,7 +490,7 @@ export class ProtoArray {
476
490
  // If the node has a parent, try to update its best-child and best-descendant.
477
491
  const parentIndex = node.parent;
478
492
  if (parentIndex !== undefined) {
479
- this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, proposerBoostRoot);
493
+ this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, this.proposerBoostRoot);
480
494
  }
481
495
  }
482
496
  // Update the previous proposer boost
@@ -801,6 +815,23 @@ export class ProtoArray {
801
815
  };
802
816
  }
803
817
 
818
+ /**
819
+ * PTC majority per vote field from the raw tallies, regardless of whether the payload is locally
820
+ * available. Returns `null` for pre-Gloas (or pruned) roots, which have no vote maps.
821
+ */
822
+ getPtcQuorum(blockRoot: RootHex): PtcQuorum | null {
823
+ const attended = this.ptcAttested.get(blockRoot);
824
+ const timelinessVotes = this.payloadTimelinessVotes.get(blockRoot);
825
+ const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
826
+ if (attended === undefined || timelinessVotes === undefined || daVotes === undefined) {
827
+ return null;
828
+ }
829
+ return {
830
+ payloadPresent: majorityVote(attended, timelinessVotes, PAYLOAD_TIMELY_THRESHOLD),
831
+ blobDataAvailable: majorityVote(attended, daVotes, DATA_AVAILABILITY_TIMELY_THRESHOLD),
832
+ };
833
+ }
834
+
804
835
  getPreviousProposerBoostRoot(): RootHex {
805
836
  return this.previousProposerBoost?.root ?? HEX_ZERO_HASH;
806
837
  }
@@ -1043,6 +1074,8 @@ export class ProtoArray {
1043
1074
 
1044
1075
  private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
1045
1076
  let nodeIndex: number | undefined = validNodeIndex;
1077
+ // Payloads that transitioned to VALID, their gloas dependents are validated below
1078
+ const validatedPayloadHashes = new Set<RootHex>();
1046
1079
  // propagate till we keep encountering syncing status
1047
1080
  while (nodeIndex !== undefined) {
1048
1081
  const node = this.getNodeFromIndex(nodeIndex);
@@ -1050,8 +1083,36 @@ export class ProtoArray {
1050
1083
  break;
1051
1084
  }
1052
1085
  this.validateNodeByIndex(nodeIndex);
1086
+ if (node.payloadStatus === PayloadStatus.FULL && node.executionPayloadBlockHash !== null) {
1087
+ validatedPayloadHashes.add(node.executionPayloadBlockHash);
1088
+ }
1053
1089
  nodeIndex = node.parent;
1054
1090
  }
1091
+
1092
+ if (validatedPayloadHashes.size > 0) {
1093
+ this.validatePayloadDependents(validatedPayloadHashes);
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * Gloas PENDING and EMPTY variants have no payload of their own, they inherit the execution status of
1099
+ * the payload they build on, which is their `executionPayloadBlockHash`. Once that payload is VALID, all
1100
+ * of them are, not only the ones on the chain the validation walked up (e.g. siblings, or blocks
1101
+ * building on a sibling's EMPTY variant).
1102
+ */
1103
+ private validatePayloadDependents(payloadHashes: Set<RootHex>): void {
1104
+ for (let nodeIndex = 0; nodeIndex < this.nodes.length; nodeIndex++) {
1105
+ const node = this.nodes[nodeIndex];
1106
+ if (
1107
+ node.payloadStatus === PayloadStatus.PENDING &&
1108
+ node.executionStatus === ExecutionStatus.Syncing &&
1109
+ node.executionPayloadBlockHash !== null &&
1110
+ payloadHashes.has(node.executionPayloadBlockHash)
1111
+ ) {
1112
+ // Also flips the sibling EMPTY variant
1113
+ this.validateNodeByIndex(nodeIndex);
1114
+ }
1115
+ }
1055
1116
  }
1056
1117
 
1057
1118
  /**
@@ -1092,6 +1153,7 @@ export class ProtoArray {
1092
1153
  this.applyScoreChanges({
1093
1154
  attestationDeltas: Array.from({length: this.nodes.length}, () => 0),
1094
1155
  proposerBoost: this.previousProposerBoost,
1156
+ proposerBoostRoot: this.proposerBoostRoot,
1095
1157
  justifiedEpoch: this.justifiedEpoch,
1096
1158
  justifiedRoot: this.justifiedRoot,
1097
1159
  finalizedEpoch: this.finalizedEpoch,