@lodestar/fork-choice 1.44.0-dev.9d8b487a59 → 1.44.0-dev.a879adb124
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/data.d.ts +4 -0
- package/lib/forkChoice/fastConfirmation/data.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/data.js +31 -0
- package/lib/forkChoice/fastConfirmation/data.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts +17 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js +129 -0
- package/lib/forkChoice/fastConfirmation/fastConfirmationRule.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/index.d.ts +4 -0
- package/lib/forkChoice/fastConfirmation/index.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/index.js +4 -0
- package/lib/forkChoice/fastConfirmation/index.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts +21 -0
- package/lib/forkChoice/fastConfirmation/metrics.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/metrics.js +42 -0
- package/lib/forkChoice/fastConfirmation/metrics.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/rules.d.ts +9 -0
- package/lib/forkChoice/fastConfirmation/rules.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/rules.js +91 -0
- package/lib/forkChoice/fastConfirmation/rules.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts +101 -0
- package/lib/forkChoice/fastConfirmation/types.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/types.js +12 -0
- package/lib/forkChoice/fastConfirmation/types.js.map +1 -0
- package/lib/forkChoice/fastConfirmation/utils.d.ts +47 -0
- package/lib/forkChoice/fastConfirmation/utils.d.ts.map +1 -0
- package/lib/forkChoice/fastConfirmation/utils.js +681 -0
- package/lib/forkChoice/fastConfirmation/utils.js.map +1 -0
- package/lib/forkChoice/forkChoice.d.ts +20 -2
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +106 -9
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +16 -2
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/safeBlocks.d.ts +2 -6
- package/lib/forkChoice/safeBlocks.d.ts.map +1 -1
- package/lib/forkChoice/safeBlocks.js +15 -7
- package/lib/forkChoice/safeBlocks.js.map +1 -1
- package/lib/forkChoice/store.d.ts +13 -2
- package/lib/forkChoice/store.d.ts.map +1 -1
- package/lib/forkChoice/store.js +29 -1
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts +12 -1
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +2 -0
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +27 -5
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +73 -17
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +7 -7
- package/src/forkChoice/fastConfirmation/data.ts +43 -0
- package/src/forkChoice/fastConfirmation/fastConfirmationRule.ts +159 -0
- package/src/forkChoice/fastConfirmation/index.ts +3 -0
- package/src/forkChoice/fastConfirmation/metrics.ts +44 -0
- package/src/forkChoice/fastConfirmation/rules.ts +124 -0
- package/src/forkChoice/fastConfirmation/types.ts +111 -0
- package/src/forkChoice/fastConfirmation/utils.ts +968 -0
- package/src/forkChoice/forkChoice.ts +133 -9
- package/src/forkChoice/interface.ts +17 -1
- package/src/forkChoice/safeBlocks.ts +15 -7
- package/src/forkChoice/store.ts +34 -1
- package/src/index.ts +11 -0
- package/src/metrics.ts +3 -1
- package/src/protoArray/protoArray.ts +85 -16
|
@@ -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`.
|
|
@@ -319,8 +346,8 @@ export class ForkChoice implements IForkChoice {
|
|
|
319
346
|
}
|
|
320
347
|
|
|
321
348
|
/** Spec: should_build_on_full(store, head) */
|
|
322
|
-
shouldBuildOnFull(head: ProtoBlock): boolean {
|
|
323
|
-
return this.protoArray.shouldBuildOnFull(head);
|
|
349
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
|
|
350
|
+
return this.protoArray.shouldBuildOnFull(head, slot);
|
|
324
351
|
}
|
|
325
352
|
|
|
326
353
|
/**
|
|
@@ -947,11 +974,12 @@ export class ForkChoice implements IForkChoice {
|
|
|
947
974
|
*/
|
|
948
975
|
notifyPtcMessages(
|
|
949
976
|
blockRoot: RootHex,
|
|
977
|
+
slot: Slot,
|
|
950
978
|
ptcIndices: number[],
|
|
951
979
|
payloadPresent: boolean,
|
|
952
980
|
blobDataAvailable: boolean
|
|
953
981
|
): void {
|
|
954
|
-
this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent, blobDataAvailable);
|
|
982
|
+
this.protoArray.notifyPtcMessages(blockRoot, slot, ptcIndices, payloadPresent, blobDataAvailable);
|
|
955
983
|
}
|
|
956
984
|
|
|
957
985
|
/**
|
|
@@ -993,12 +1021,12 @@ export class ForkChoice implements IForkChoice {
|
|
|
993
1021
|
const previousSlot = this.fcStore.currentSlot;
|
|
994
1022
|
// Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
|
|
995
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();
|
|
996
1029
|
}
|
|
997
|
-
|
|
998
|
-
this.queuedAttestationsPreviousSlot = 0;
|
|
999
|
-
// Process any attestations that might now be eligible.
|
|
1000
|
-
this.processAttestationQueue();
|
|
1001
|
-
this.validatedAttestationDatas = new Set();
|
|
1002
1030
|
}
|
|
1003
1031
|
|
|
1004
1032
|
getTime(): Slot {
|
|
@@ -1067,6 +1095,30 @@ export class ForkChoice implements IForkChoice {
|
|
|
1067
1095
|
return votes.toBoolArray().map((v) => v ?? null);
|
|
1068
1096
|
}
|
|
1069
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
|
+
|
|
1070
1122
|
/**
|
|
1071
1123
|
* Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
1072
1124
|
*/
|
|
@@ -1892,6 +1944,78 @@ export class ForkChoice implements IForkChoice {
|
|
|
1892
1944
|
|
|
1893
1945
|
return {prelimProposerHead};
|
|
1894
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
|
+
}
|
|
1895
2019
|
}
|
|
1896
2020
|
|
|
1897
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
|
*
|
|
@@ -194,6 +200,7 @@ export interface IForkChoice {
|
|
|
194
200
|
*/
|
|
195
201
|
notifyPtcMessages(
|
|
196
202
|
blockRoot: RootHex,
|
|
203
|
+
slot: Slot,
|
|
197
204
|
ptcIndices: number[],
|
|
198
205
|
payloadPresent: boolean,
|
|
199
206
|
blobDataAvailable: boolean
|
|
@@ -246,6 +253,15 @@ export interface IForkChoice {
|
|
|
246
253
|
hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
|
|
247
254
|
getSlotsPresent(windowStart: number): number;
|
|
248
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
|
+
|
|
249
265
|
/**
|
|
250
266
|
* Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
251
267
|
*/
|
|
@@ -256,7 +272,7 @@ export interface IForkChoice {
|
|
|
256
272
|
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null;
|
|
257
273
|
shouldExtendPayload(blockRoot: RootHex): boolean;
|
|
258
274
|
/** Spec: should_build_on_full(store, head) */
|
|
259
|
-
shouldBuildOnFull(head: ProtoBlock): boolean;
|
|
275
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean;
|
|
260
276
|
getFinalizedBlock(): ProtoBlock;
|
|
261
277
|
getJustifiedBlock(): ProtoBlock;
|
|
262
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/src/forkChoice/store.ts
CHANGED
|
@@ -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",
|
|
@@ -91,14 +91,14 @@ export class ProtoArray {
|
|
|
91
91
|
*
|
|
92
92
|
* Bit i = PTC member i voted payloadPresent=true (timeliness YES vote)
|
|
93
93
|
*/
|
|
94
|
-
private
|
|
94
|
+
private payloadTimelinessVotes = new Map<RootHex, BitArray>();
|
|
95
95
|
/**
|
|
96
96
|
* Blob data availability votes per block.
|
|
97
97
|
* Spec: gloas/fork-choice.md#modified-store (payload_data_availability_vote)
|
|
98
98
|
*
|
|
99
99
|
* Bit i = PTC member i voted blobDataAvailable=true (DA YES vote)
|
|
100
100
|
*/
|
|
101
|
-
private
|
|
101
|
+
private payloadDataAvailabilityVotes = new Map<RootHex, BitArray>();
|
|
102
102
|
/**
|
|
103
103
|
* Tracks which PTC members have attested at all (any payload_status).
|
|
104
104
|
* Without this, we cannot tell "didn't vote" (None) from "voted false" —
|
|
@@ -552,9 +552,9 @@ export class ProtoArray {
|
|
|
552
552
|
|
|
553
553
|
// Initialize PTC vote bitvectors for this block.
|
|
554
554
|
// Spec: gloas/fork-choice.md#modified-on_block
|
|
555
|
-
this.
|
|
555
|
+
this.payloadTimelinessVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
556
556
|
this.ptcAttested.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
557
|
-
this.
|
|
557
|
+
this.payloadDataAvailabilityVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
558
558
|
} else {
|
|
559
559
|
// Pre-Gloas: Only create FULL node (payload embedded in block)
|
|
560
560
|
const node: ProtoNode = {
|
|
@@ -678,18 +678,26 @@ export class ProtoArray {
|
|
|
678
678
|
*/
|
|
679
679
|
notifyPtcMessages(
|
|
680
680
|
blockRoot: RootHex,
|
|
681
|
+
slot: Slot,
|
|
681
682
|
ptcIndices: number[],
|
|
682
683
|
payloadPresent: boolean,
|
|
683
684
|
blobDataAvailable: boolean
|
|
684
685
|
): void {
|
|
685
|
-
const votes = this.
|
|
686
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
686
687
|
const attended = this.ptcAttested.get(blockRoot);
|
|
687
|
-
const daVotes = this.
|
|
688
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
688
689
|
if (votes === undefined || attended === undefined || daVotes === undefined) {
|
|
689
690
|
// Block not found or not a Gloas block, ignore
|
|
690
691
|
return;
|
|
691
692
|
}
|
|
692
693
|
|
|
694
|
+
// PTC votes can only change the vote for their assigned beacon block, return early otherwise
|
|
695
|
+
const nodeIndex = this.getDefaultNodeIndex(blockRoot);
|
|
696
|
+
const node = nodeIndex !== undefined ? this.getNodeByIndex(nodeIndex) : undefined;
|
|
697
|
+
if (node === undefined || node.slot !== slot) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
693
701
|
for (const ptcIndex of ptcIndices) {
|
|
694
702
|
if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
|
|
695
703
|
throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
|
|
@@ -701,7 +709,7 @@ export class ProtoArray {
|
|
|
701
709
|
}
|
|
702
710
|
|
|
703
711
|
getPTCVotes(blockRootHex: RootHex): BitArray | null {
|
|
704
|
-
const votes = this.
|
|
712
|
+
const votes = this.payloadTimelinessVotes.get(blockRootHex);
|
|
705
713
|
if (votes === undefined) {
|
|
706
714
|
// Block not found or not a Gloas block
|
|
707
715
|
return null;
|
|
@@ -710,11 +718,63 @@ export class ProtoArray {
|
|
|
710
718
|
return votes;
|
|
711
719
|
}
|
|
712
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
|
+
|
|
713
773
|
/**
|
|
714
774
|
* Spec: payload_timeliness(store, root, timely=True)
|
|
715
775
|
*/
|
|
716
776
|
isPayloadTimely(blockRoot: RootHex): boolean {
|
|
717
|
-
const votes = this.
|
|
777
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
718
778
|
if (votes === undefined) return false;
|
|
719
779
|
if (!this.hasPayload(blockRoot)) return false;
|
|
720
780
|
return bitCount(votes.uint8Array) > PAYLOAD_TIMELY_THRESHOLD;
|
|
@@ -724,7 +784,7 @@ export class ProtoArray {
|
|
|
724
784
|
* Spec: payload_timeliness(store, root, timely=False)
|
|
725
785
|
*/
|
|
726
786
|
isPayloadNotTimely(blockRoot: RootHex): boolean {
|
|
727
|
-
const votes = this.
|
|
787
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
728
788
|
const attended = this.ptcAttested.get(blockRoot);
|
|
729
789
|
if (votes === undefined || attended === undefined) return false;
|
|
730
790
|
// Spec: not verified locally → returns `not False = True`
|
|
@@ -736,7 +796,7 @@ export class ProtoArray {
|
|
|
736
796
|
* Spec: payload_data_availability(store, root, available=True)
|
|
737
797
|
*/
|
|
738
798
|
isPayloadDataAvailable(blockRoot: RootHex): boolean {
|
|
739
|
-
const daVotes = this.
|
|
799
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
740
800
|
if (daVotes === undefined) return false;
|
|
741
801
|
if (!this.hasPayload(blockRoot)) return false;
|
|
742
802
|
return bitCount(daVotes.uint8Array) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
|
|
@@ -746,7 +806,7 @@ export class ProtoArray {
|
|
|
746
806
|
* Spec: payload_data_availability(store, root, available=False)
|
|
747
807
|
*/
|
|
748
808
|
isPayloadDataNotAvailable(blockRoot: RootHex): boolean {
|
|
749
|
-
const daVotes = this.
|
|
809
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
750
810
|
const attended = this.ptcAttested.get(blockRoot);
|
|
751
811
|
if (daVotes === undefined || attended === undefined) return false;
|
|
752
812
|
// Spec: not verified locally → returns `not False = True`
|
|
@@ -758,14 +818,23 @@ export class ProtoArray {
|
|
|
758
818
|
* Spec: should_build_on_full(store, head)
|
|
759
819
|
*
|
|
760
820
|
* The proposer is forced to build on the EMPTY variant (effectively reorging)
|
|
761
|
-
* 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.
|
|
762
823
|
*/
|
|
763
|
-
shouldBuildOnFull(head: ProtoBlock): boolean {
|
|
824
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
|
|
764
825
|
if (head.payloadStatus === PayloadStatus.PENDING) {
|
|
765
826
|
throw new Error("shouldBuildOnFull called with PENDING head");
|
|
766
827
|
}
|
|
767
828
|
if (head.payloadStatus === PayloadStatus.EMPTY) return false;
|
|
768
|
-
|
|
829
|
+
|
|
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.
|
|
833
|
+
if (head.slot + 1 !== slot) return true;
|
|
834
|
+
|
|
835
|
+
if (this.isPayloadDataNotAvailable(head.blockRoot)) return false;
|
|
836
|
+
|
|
837
|
+
return !this.isPayloadNotTimely(head.blockRoot);
|
|
769
838
|
}
|
|
770
839
|
|
|
771
840
|
/**
|
|
@@ -1205,9 +1274,9 @@ export class ProtoArray {
|
|
|
1205
1274
|
this.indices.delete(root);
|
|
1206
1275
|
// Prune PTC votes for this block to prevent memory leak
|
|
1207
1276
|
// Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
|
|
1208
|
-
this.
|
|
1277
|
+
this.payloadTimelinessVotes.delete(root);
|
|
1209
1278
|
this.ptcAttested.delete(root);
|
|
1210
|
-
this.
|
|
1279
|
+
this.payloadDataAvailabilityVotes.delete(root);
|
|
1211
1280
|
}
|
|
1212
1281
|
|
|
1213
1282
|
// Store nodes prior to finalization
|