@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.
- package/lib/forkChoice/fastConfirmation/types.d.ts +3 -1
- package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.d.ts +2 -2
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -1
- package/lib/forkChoice/fastConfirmation/utils.js +8 -3
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +2 -1
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +37 -3
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +9 -1
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/forkChoice/store.d.ts +4 -1
- package/lib/forkChoice/store.d.ts.map +1 -1
- package/lib/forkChoice/store.js +3 -0
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +3 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +5 -0
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/interface.d.ts +15 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/interface.js +13 -0
- package/lib/protoArray/interface.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +18 -2
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +57 -3
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +8 -8
- package/src/forkChoice/fastConfirmation/types.ts +3 -2
- package/src/forkChoice/fastConfirmation/utils.ts +12 -5
- package/src/forkChoice/forkChoice.ts +38 -2
- package/src/forkChoice/interface.ts +6 -0
- package/src/forkChoice/store.ts +7 -1
- package/src/index.ts +3 -1
- package/src/metrics.ts +5 -0
- package/src/protoArray/interface.ts +21 -0
- 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
|
|
package/src/forkChoice/store.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|