@lodestar/fork-choice 1.50.0-dev.5866a397b6 → 1.50.0-dev.58d40975f9

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 (47) hide show
  1. package/lib/forkChoice/fastConfirmation/rules.d.ts.map +1 -1
  2. package/lib/forkChoice/fastConfirmation/rules.js +4 -1
  3. package/lib/forkChoice/fastConfirmation/rules.js.map +1 -1
  4. package/lib/forkChoice/fastConfirmation/types.d.ts +3 -1
  5. package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
  6. package/lib/forkChoice/fastConfirmation/utils.d.ts +2 -2
  7. package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
  8. package/lib/forkChoice/fastConfirmation/utils.js +8 -3
  9. package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
  10. package/lib/forkChoice/forkChoice.d.ts +2 -1
  11. package/lib/forkChoice/forkChoice.d.ts.map +1 -1
  12. package/lib/forkChoice/forkChoice.js +60 -5
  13. package/lib/forkChoice/forkChoice.js.map +1 -1
  14. package/lib/forkChoice/interface.d.ts +9 -1
  15. package/lib/forkChoice/interface.d.ts.map +1 -1
  16. package/lib/forkChoice/interface.js.map +1 -1
  17. package/lib/forkChoice/store.d.ts +4 -1
  18. package/lib/forkChoice/store.d.ts.map +1 -1
  19. package/lib/forkChoice/store.js +3 -0
  20. package/lib/forkChoice/store.js.map +1 -1
  21. package/lib/index.d.ts +3 -3
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/index.js +1 -1
  24. package/lib/index.js.map +1 -1
  25. package/lib/metrics.d.ts +3 -0
  26. package/lib/metrics.d.ts.map +1 -1
  27. package/lib/metrics.js +5 -0
  28. package/lib/metrics.js.map +1 -1
  29. package/lib/protoArray/interface.d.ts +15 -0
  30. package/lib/protoArray/interface.d.ts.map +1 -1
  31. package/lib/protoArray/interface.js +13 -0
  32. package/lib/protoArray/interface.js.map +1 -1
  33. package/lib/protoArray/protoArray.d.ts +13 -1
  34. package/lib/protoArray/protoArray.d.ts.map +1 -1
  35. package/lib/protoArray/protoArray.js +49 -0
  36. package/lib/protoArray/protoArray.js.map +1 -1
  37. package/package.json +8 -8
  38. package/src/forkChoice/fastConfirmation/rules.ts +3 -0
  39. package/src/forkChoice/fastConfirmation/types.ts +3 -2
  40. package/src/forkChoice/fastConfirmation/utils.ts +12 -5
  41. package/src/forkChoice/forkChoice.ts +65 -6
  42. package/src/forkChoice/interface.ts +6 -0
  43. package/src/forkChoice/store.ts +7 -1
  44. package/src/index.ts +3 -1
  45. package/src/metrics.ts +5 -0
  46. package/src/protoArray/interface.ts +21 -0
  47. package/src/protoArray/protoArray.ts +54 -0
@@ -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
 
@@ -801,6 +808,23 @@ export class ProtoArray {
801
808
  };
802
809
  }
803
810
 
811
+ /**
812
+ * PTC majority per vote field from the raw tallies, regardless of whether the payload is locally
813
+ * available. Returns `null` for pre-Gloas (or pruned) roots, which have no vote maps.
814
+ */
815
+ getPtcQuorum(blockRoot: RootHex): PtcQuorum | null {
816
+ const attended = this.ptcAttested.get(blockRoot);
817
+ const timelinessVotes = this.payloadTimelinessVotes.get(blockRoot);
818
+ const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
819
+ if (attended === undefined || timelinessVotes === undefined || daVotes === undefined) {
820
+ return null;
821
+ }
822
+ return {
823
+ payloadPresent: majorityVote(attended, timelinessVotes, PAYLOAD_TIMELY_THRESHOLD),
824
+ blobDataAvailable: majorityVote(attended, daVotes, DATA_AVAILABILITY_TIMELY_THRESHOLD),
825
+ };
826
+ }
827
+
804
828
  getPreviousProposerBoostRoot(): RootHex {
805
829
  return this.previousProposerBoost?.root ?? HEX_ZERO_HASH;
806
830
  }
@@ -1043,6 +1067,8 @@ export class ProtoArray {
1043
1067
 
1044
1068
  private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
1045
1069
  let nodeIndex: number | undefined = validNodeIndex;
1070
+ // Payloads that transitioned to VALID, their gloas dependents are validated below
1071
+ const validatedPayloadHashes = new Set<RootHex>();
1046
1072
  // propagate till we keep encountering syncing status
1047
1073
  while (nodeIndex !== undefined) {
1048
1074
  const node = this.getNodeFromIndex(nodeIndex);
@@ -1050,8 +1076,36 @@ export class ProtoArray {
1050
1076
  break;
1051
1077
  }
1052
1078
  this.validateNodeByIndex(nodeIndex);
1079
+ if (node.payloadStatus === PayloadStatus.FULL && node.executionPayloadBlockHash !== null) {
1080
+ validatedPayloadHashes.add(node.executionPayloadBlockHash);
1081
+ }
1053
1082
  nodeIndex = node.parent;
1054
1083
  }
1084
+
1085
+ if (validatedPayloadHashes.size > 0) {
1086
+ this.validatePayloadDependents(validatedPayloadHashes);
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Gloas PENDING and EMPTY variants have no payload of their own, they inherit the execution status of
1092
+ * the payload they build on, which is their `executionPayloadBlockHash`. Once that payload is VALID, all
1093
+ * of them are, not only the ones on the chain the validation walked up (e.g. siblings, or blocks
1094
+ * building on a sibling's EMPTY variant).
1095
+ */
1096
+ private validatePayloadDependents(payloadHashes: Set<RootHex>): void {
1097
+ for (let nodeIndex = 0; nodeIndex < this.nodes.length; nodeIndex++) {
1098
+ const node = this.nodes[nodeIndex];
1099
+ if (
1100
+ node.payloadStatus === PayloadStatus.PENDING &&
1101
+ node.executionStatus === ExecutionStatus.Syncing &&
1102
+ node.executionPayloadBlockHash !== null &&
1103
+ payloadHashes.has(node.executionPayloadBlockHash)
1104
+ ) {
1105
+ // Also flips the sibling EMPTY variant
1106
+ this.validateNodeByIndex(nodeIndex);
1107
+ }
1108
+ }
1055
1109
  }
1056
1110
 
1057
1111
  /**