@lodestar/fork-choice 1.35.0-dev.f80d2d52da → 1.35.0-dev.fcf8d024ea
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/errors.d.ts.map +1 -0
- package/lib/forkChoice/forkChoice.d.ts +1 -1
- package/lib/forkChoice/forkChoice.d.ts.map +1 -0
- package/lib/forkChoice/forkChoice.js +50 -32
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +1 -2
- package/lib/forkChoice/interface.d.ts.map +1 -0
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/forkChoice/store.d.ts.map +1 -0
- package/lib/forkChoice/store.js +8 -1
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +6 -6
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -4
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts.map +1 -0
- package/lib/protoArray/computeDeltas.d.ts.map +1 -0
- package/lib/protoArray/errors.d.ts.map +1 -0
- package/lib/protoArray/interface.d.ts.map +1 -0
- package/lib/protoArray/protoArray.d.ts.map +1 -0
- package/lib/protoArray/protoArray.js +11 -3
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +16 -13
- package/src/forkChoice/errors.ts +98 -0
- package/src/forkChoice/forkChoice.ts +1651 -0
- package/src/forkChoice/interface.ts +269 -0
- package/src/forkChoice/store.ts +124 -0
- package/src/index.ts +34 -0
- package/src/metrics.ts +71 -0
- package/src/protoArray/computeDeltas.ts +97 -0
- package/src/protoArray/errors.ts +59 -0
- package/src/protoArray/interface.ts +102 -0
- package/src/protoArray/protoArray.ts +1076 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CachedBeaconStateAllForks,
|
|
3
|
+
DataAvailabilityStatus,
|
|
4
|
+
EffectiveBalanceIncrements,
|
|
5
|
+
} from "@lodestar/state-transition";
|
|
6
|
+
import {
|
|
7
|
+
AttesterSlashing,
|
|
8
|
+
BeaconBlock,
|
|
9
|
+
Epoch,
|
|
10
|
+
IndexedAttestation,
|
|
11
|
+
Root,
|
|
12
|
+
RootHex,
|
|
13
|
+
Slot,
|
|
14
|
+
ValidatorIndex,
|
|
15
|
+
} from "@lodestar/types";
|
|
16
|
+
import {LVHExecResponse, MaybeValidExecutionStatus, ProtoBlock, ProtoNode} from "../protoArray/interface.js";
|
|
17
|
+
import {UpdateAndGetHeadOpt} from "./forkChoice.js";
|
|
18
|
+
import {CheckpointWithHex} from "./store.js";
|
|
19
|
+
|
|
20
|
+
export type CheckpointHex = {
|
|
21
|
+
epoch: Epoch;
|
|
22
|
+
root: RootHex;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CheckpointsWithHex = {
|
|
26
|
+
justifiedCheckpoint: CheckpointWithHex;
|
|
27
|
+
finalizedCheckpoint: CheckpointWithHex;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CheckpointHexWithBalance = {
|
|
31
|
+
checkpoint: CheckpointWithHex;
|
|
32
|
+
balances: EffectiveBalanceIncrements;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type CheckpointHexWithTotalBalance = CheckpointHexWithBalance & {
|
|
36
|
+
totalBalance: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export enum EpochDifference {
|
|
40
|
+
current = 0,
|
|
41
|
+
previous = 1,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export enum AncestorStatus {
|
|
45
|
+
CommonAncestor,
|
|
46
|
+
Descendant,
|
|
47
|
+
NoCommonAncenstor,
|
|
48
|
+
BlockUnknown,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AncestorResult =
|
|
52
|
+
| {code: AncestorStatus.CommonAncestor; depth: number}
|
|
53
|
+
| {code: AncestorStatus.Descendant}
|
|
54
|
+
| {code: AncestorStatus.NoCommonAncenstor}
|
|
55
|
+
| {code: AncestorStatus.BlockUnknown};
|
|
56
|
+
|
|
57
|
+
// Reason for not proposer boost reorging
|
|
58
|
+
export enum NotReorgedReason {
|
|
59
|
+
HeadBlockIsTimely = "headBlockIsTimely",
|
|
60
|
+
ParentBlockNotAvailable = "parentBlockNotAvailable",
|
|
61
|
+
ProposerBoostReorgDisabled = "proposerBoostReorgDisabled",
|
|
62
|
+
NotShufflingStable = "notShufflingStable",
|
|
63
|
+
NotFFGCompetitive = "notFFGCompetitive",
|
|
64
|
+
ChainLongUnfinality = "chainLongUnfinality",
|
|
65
|
+
ParentBlockDistanceMoreThanOneSlot = "parentBlockDistanceMoreThanOneSlot",
|
|
66
|
+
ReorgMoreThanOneSlot = "reorgMoreThanOneSlot",
|
|
67
|
+
ProposerBoostNotWornOff = "proposerBoostNotWornOff",
|
|
68
|
+
HeadBlockNotWeak = "headBlockNotWeak",
|
|
69
|
+
ParentBlockNotStrong = "parentBlockNotStrong",
|
|
70
|
+
NotProposingOnTime = "notProposingOnTime",
|
|
71
|
+
NotProposerOfNextSlot = "notProposerOfNextSlot",
|
|
72
|
+
HeadBlockNotAvailable = "headBlockNotAvailable", // Should not happen because head block should be in cache
|
|
73
|
+
Unknown = "unknown", // A placeholder in case reason is not provided
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ShouldOverrideForkChoiceUpdateResult =
|
|
77
|
+
| {shouldOverrideFcu: true; parentBlock: ProtoBlock}
|
|
78
|
+
| {shouldOverrideFcu: false; reason: NotReorgedReason};
|
|
79
|
+
|
|
80
|
+
export interface IForkChoice {
|
|
81
|
+
irrecoverableError?: Error;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers
|
|
85
|
+
* to the block that is *returned*, not the one that is supplied.)
|
|
86
|
+
*
|
|
87
|
+
* ## Specification
|
|
88
|
+
*
|
|
89
|
+
* Equivalent to:
|
|
90
|
+
*
|
|
91
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
|
|
92
|
+
*/
|
|
93
|
+
getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex;
|
|
94
|
+
/**
|
|
95
|
+
* Run the fork choice rule to determine the head.
|
|
96
|
+
*
|
|
97
|
+
* ## Specification
|
|
98
|
+
*
|
|
99
|
+
* Is equivalent to:
|
|
100
|
+
*
|
|
101
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_head
|
|
102
|
+
*/
|
|
103
|
+
getHeadRoot(): RootHex;
|
|
104
|
+
getHead(): ProtoBlock;
|
|
105
|
+
updateAndGetHead(mode: UpdateAndGetHeadOpt): {
|
|
106
|
+
head: ProtoBlock;
|
|
107
|
+
isHeadTimely?: boolean;
|
|
108
|
+
notReorgedReason?: NotReorgedReason;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* This is called during block import when proposerBoostReorg is enabled
|
|
112
|
+
* fcu call in `importBlock()` will be suppressed if this returns true. It is also
|
|
113
|
+
* called by `predictProposerHead()` during `prepareNextSlot()`.
|
|
114
|
+
*/
|
|
115
|
+
shouldOverrideForkChoiceUpdate(
|
|
116
|
+
blockRoot: RootHex,
|
|
117
|
+
secFromSlot: number,
|
|
118
|
+
currentSlot: Slot
|
|
119
|
+
): ShouldOverrideForkChoiceUpdateResult;
|
|
120
|
+
/**
|
|
121
|
+
* Retrieves all possible chain heads (leaves of fork choice tree).
|
|
122
|
+
*/
|
|
123
|
+
getHeads(): ProtoBlock[];
|
|
124
|
+
/**
|
|
125
|
+
* Retrieve all nodes for the debug API.
|
|
126
|
+
*/
|
|
127
|
+
getAllNodes(): ProtoNode[];
|
|
128
|
+
getFinalizedCheckpoint(): CheckpointWithHex;
|
|
129
|
+
getJustifiedCheckpoint(): CheckpointWithHex;
|
|
130
|
+
/**
|
|
131
|
+
* Add `block` to the fork choice DAG.
|
|
132
|
+
*
|
|
133
|
+
* ## Specification
|
|
134
|
+
*
|
|
135
|
+
* Approximates:
|
|
136
|
+
*
|
|
137
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_block
|
|
138
|
+
*
|
|
139
|
+
* It only approximates the specification since it does not run the `state_transition` check.
|
|
140
|
+
* That should have already been called upstream and it's too expensive to call again.
|
|
141
|
+
*
|
|
142
|
+
* ## Notes:
|
|
143
|
+
*
|
|
144
|
+
* The supplied block **must** pass the `state_transition` function as it will not be run here.
|
|
145
|
+
*/
|
|
146
|
+
onBlock(
|
|
147
|
+
block: BeaconBlock,
|
|
148
|
+
state: CachedBeaconStateAllForks,
|
|
149
|
+
blockDelaySec: number,
|
|
150
|
+
currentSlot: Slot,
|
|
151
|
+
executionStatus: MaybeValidExecutionStatus,
|
|
152
|
+
dataAvailabilityStatus: DataAvailabilityStatus
|
|
153
|
+
): ProtoBlock;
|
|
154
|
+
/**
|
|
155
|
+
* Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
|
|
156
|
+
*
|
|
157
|
+
* ## Specification
|
|
158
|
+
*
|
|
159
|
+
* Approximates:
|
|
160
|
+
*
|
|
161
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
|
|
162
|
+
*
|
|
163
|
+
* It only approximates the specification since it does not perform
|
|
164
|
+
* `is_valid_indexed_attestation` since that should already have been called upstream and it's
|
|
165
|
+
* too expensive to call again.
|
|
166
|
+
*
|
|
167
|
+
* ## Notes:
|
|
168
|
+
*
|
|
169
|
+
* The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
|
|
170
|
+
* will not be run here.
|
|
171
|
+
*/
|
|
172
|
+
onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void;
|
|
173
|
+
/**
|
|
174
|
+
* Register attester slashing in order not to consider their votes in `getHead`
|
|
175
|
+
*
|
|
176
|
+
* ## Specification
|
|
177
|
+
*
|
|
178
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#on_attester_slashing
|
|
179
|
+
*/
|
|
180
|
+
onAttesterSlashing(slashing: AttesterSlashing): void;
|
|
181
|
+
getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
|
|
184
|
+
*/
|
|
185
|
+
updateTime(currentSlot: Slot): void;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns current time slot.
|
|
189
|
+
*/
|
|
190
|
+
getTime(): Slot;
|
|
191
|
+
/**
|
|
192
|
+
* Returns `true` if the block is known **and** a descendant of the finalized root.
|
|
193
|
+
*/
|
|
194
|
+
hasBlock(blockRoot: Root): boolean;
|
|
195
|
+
hasBlockHex(blockRoot: RootHex): boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Same to hasBlock, but without checking if the block is a descendant of the finalized root.
|
|
198
|
+
*/
|
|
199
|
+
hasBlockUnsafe(blockRoot: Root): boolean;
|
|
200
|
+
hasBlockHexUnsafe(blockRoot: RootHex): boolean;
|
|
201
|
+
getSlotsPresent(windowStart: number): number;
|
|
202
|
+
/**
|
|
203
|
+
* Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
204
|
+
*/
|
|
205
|
+
getBlock(blockRoot: Root): ProtoBlock | null;
|
|
206
|
+
getBlockHex(blockRoot: RootHex): ProtoBlock | null;
|
|
207
|
+
getFinalizedBlock(): ProtoBlock;
|
|
208
|
+
getJustifiedBlock(): ProtoBlock;
|
|
209
|
+
/**
|
|
210
|
+
* Returns true if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
211
|
+
*
|
|
212
|
+
* Always returns `false` if either input roots are unknown.
|
|
213
|
+
* Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
|
|
214
|
+
*/
|
|
215
|
+
isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Prune items up to a finalized root.
|
|
218
|
+
*/
|
|
219
|
+
prune(finalizedRoot: RootHex): ProtoBlock[];
|
|
220
|
+
setPruneThreshold(threshold: number): void;
|
|
221
|
+
/**
|
|
222
|
+
* Iterates backwards through ancestor block summaries, starting from a block root
|
|
223
|
+
*/
|
|
224
|
+
iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<ProtoBlock>;
|
|
225
|
+
getAllAncestorBlocks(blockRoot: RootHex): ProtoBlock[];
|
|
226
|
+
/**
|
|
227
|
+
* The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
|
|
228
|
+
*/
|
|
229
|
+
getAllNonAncestorBlocks(blockRoot: RootHex): ProtoBlock[];
|
|
230
|
+
/**
|
|
231
|
+
* Returns both ancestor and non-ancestor blocks in a single traversal.
|
|
232
|
+
*/
|
|
233
|
+
getAllAncestorAndNonAncestorBlocks(blockRoot: RootHex): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]};
|
|
234
|
+
getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null;
|
|
235
|
+
getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null;
|
|
236
|
+
/**
|
|
237
|
+
* Returns all ProtoBlock known to fork-choice. Must not mutated the returned array
|
|
238
|
+
*/
|
|
239
|
+
forwarditerateAncestorBlocks(): ProtoBlock[];
|
|
240
|
+
/**
|
|
241
|
+
* Iterates forward descendants of blockRoot. Does not yield blockRoot itself
|
|
242
|
+
*/
|
|
243
|
+
forwardIterateDescendants(blockRoot: RootHex): IterableIterator<ProtoBlock>;
|
|
244
|
+
getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[];
|
|
245
|
+
getBlockSummariesAtSlot(slot: Slot): ProtoBlock[];
|
|
246
|
+
/** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
|
|
247
|
+
getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult;
|
|
248
|
+
/**
|
|
249
|
+
* Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided
|
|
250
|
+
*/
|
|
251
|
+
validateLatestHash(execResponse: LVHExecResponse): void;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A dependent root is the block root of the last block before the state transition that decided a specific shuffling
|
|
255
|
+
*/
|
|
256
|
+
getDependentRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Same to the PowBlock but we want RootHex to work with forkchoice conveniently */
|
|
260
|
+
export type PowBlockHex = {
|
|
261
|
+
blockHash: RootHex;
|
|
262
|
+
parentHash: RootHex;
|
|
263
|
+
totalDifficulty: bigint;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export type LatestMessage = {
|
|
267
|
+
epoch: Epoch;
|
|
268
|
+
root: RootHex;
|
|
269
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {CachedBeaconStateAllForks, EffectiveBalanceIncrements} from "@lodestar/state-transition";
|
|
2
|
+
import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
|
|
3
|
+
import {toRootHex} from "@lodestar/utils";
|
|
4
|
+
import {CheckpointHexWithBalance, CheckpointHexWithTotalBalance} from "./interface.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stores checkpoints in a hybrid format:
|
|
8
|
+
* - Original checkpoint for fast consumption in Lodestar's side
|
|
9
|
+
* - Root in string hex for fast comparisons inside the fork-choice
|
|
10
|
+
*/
|
|
11
|
+
export type CheckpointWithHex = phase0.Checkpoint & {rootHex: RootHex};
|
|
12
|
+
|
|
13
|
+
export type JustifiedBalances = EffectiveBalanceIncrements;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the justified balances of checkpoint.
|
|
17
|
+
* MUST not throw an error in any case, related to cache miss. Either trigger regen or approximate from a close state.
|
|
18
|
+
* `blockState` is maybe used as a fallback state to get balances since it's very close to desired justified state.
|
|
19
|
+
* @param blockState state that declares justified checkpoint `checkpoint`
|
|
20
|
+
*/
|
|
21
|
+
export type JustifiedBalancesGetter = (
|
|
22
|
+
checkpoint: CheckpointWithHex,
|
|
23
|
+
blockState: CachedBeaconStateAllForks
|
|
24
|
+
) => JustifiedBalances;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Approximates the `Store` in "Ethereum Consensus -- Beacon Chain Fork Choice":
|
|
28
|
+
*
|
|
29
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#store
|
|
30
|
+
*
|
|
31
|
+
* ## Detail
|
|
32
|
+
*
|
|
33
|
+
* This is only an approximation for two reasons:
|
|
34
|
+
*
|
|
35
|
+
* - The actual block DAG in `ProtoArray`.
|
|
36
|
+
* - `time` is represented using `Slot` instead of UNIX epoch `u64`.
|
|
37
|
+
*/
|
|
38
|
+
export interface IForkChoiceStore {
|
|
39
|
+
currentSlot: Slot;
|
|
40
|
+
get justified(): CheckpointHexWithTotalBalance;
|
|
41
|
+
set justified(justified: CheckpointHexWithBalance);
|
|
42
|
+
unrealizedJustified: CheckpointHexWithBalance;
|
|
43
|
+
finalizedCheckpoint: CheckpointWithHex;
|
|
44
|
+
unrealizedFinalizedCheckpoint: CheckpointWithHex;
|
|
45
|
+
justifiedBalancesGetter: JustifiedBalancesGetter;
|
|
46
|
+
equivocatingIndices: Set<ValidatorIndex>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* IForkChoiceStore implementer which emits forkChoice events on updated justified and finalized checkpoints.
|
|
51
|
+
*/
|
|
52
|
+
export class ForkChoiceStore implements IForkChoiceStore {
|
|
53
|
+
private _justified: CheckpointHexWithTotalBalance;
|
|
54
|
+
unrealizedJustified: CheckpointHexWithBalance;
|
|
55
|
+
private _finalizedCheckpoint: CheckpointWithHex;
|
|
56
|
+
unrealizedFinalizedCheckpoint: CheckpointWithHex;
|
|
57
|
+
equivocatingIndices = new Set<ValidatorIndex>();
|
|
58
|
+
justifiedBalancesGetter: JustifiedBalancesGetter;
|
|
59
|
+
currentSlot: Slot;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
currentSlot: Slot,
|
|
63
|
+
justifiedCheckpoint: phase0.Checkpoint,
|
|
64
|
+
finalizedCheckpoint: phase0.Checkpoint,
|
|
65
|
+
justifiedBalances: EffectiveBalanceIncrements,
|
|
66
|
+
justifiedBalancesGetter: JustifiedBalancesGetter,
|
|
67
|
+
private readonly events?: {
|
|
68
|
+
onJustified: (cp: CheckpointWithHex) => void;
|
|
69
|
+
onFinalized: (cp: CheckpointWithHex) => void;
|
|
70
|
+
}
|
|
71
|
+
) {
|
|
72
|
+
this.justifiedBalancesGetter = justifiedBalancesGetter;
|
|
73
|
+
this.currentSlot = currentSlot;
|
|
74
|
+
const justified = {
|
|
75
|
+
checkpoint: toCheckpointWithHex(justifiedCheckpoint),
|
|
76
|
+
balances: justifiedBalances,
|
|
77
|
+
totalBalance: computeTotalBalance(justifiedBalances),
|
|
78
|
+
};
|
|
79
|
+
this._justified = justified;
|
|
80
|
+
this.unrealizedJustified = justified;
|
|
81
|
+
this._finalizedCheckpoint = toCheckpointWithHex(finalizedCheckpoint);
|
|
82
|
+
this.unrealizedFinalizedCheckpoint = this._finalizedCheckpoint;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get justified(): CheckpointHexWithTotalBalance {
|
|
86
|
+
return this._justified;
|
|
87
|
+
}
|
|
88
|
+
set justified(justified: CheckpointHexWithBalance) {
|
|
89
|
+
this._justified = {...justified, totalBalance: computeTotalBalance(justified.balances)};
|
|
90
|
+
this.events?.onJustified(justified.checkpoint);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get finalizedCheckpoint(): CheckpointWithHex {
|
|
94
|
+
return this._finalizedCheckpoint;
|
|
95
|
+
}
|
|
96
|
+
set finalizedCheckpoint(checkpoint: CheckpointWithHex) {
|
|
97
|
+
const cp = toCheckpointWithHex(checkpoint);
|
|
98
|
+
this._finalizedCheckpoint = cp;
|
|
99
|
+
this.events?.onFinalized(cp);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function toCheckpointWithHex(checkpoint: phase0.Checkpoint): CheckpointWithHex {
|
|
104
|
+
// `valueOf` coerses the checkpoint, which may be tree-backed, into a javascript object
|
|
105
|
+
// See https://github.com/ChainSafe/lodestar/issues/2258
|
|
106
|
+
const root = checkpoint.root;
|
|
107
|
+
return {
|
|
108
|
+
epoch: checkpoint.epoch,
|
|
109
|
+
root,
|
|
110
|
+
rootHex: toRootHex(root),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function equalCheckpointWithHex(a: CheckpointWithHex, b: CheckpointWithHex): boolean {
|
|
115
|
+
return a.epoch === b.epoch && a.rootHex === b.rootHex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function computeTotalBalance(balances: EffectiveBalanceIncrements): number {
|
|
119
|
+
let totalBalance = 0;
|
|
120
|
+
for (let i = 0; i < balances.length; i++) {
|
|
121
|
+
totalBalance += balances[i];
|
|
122
|
+
}
|
|
123
|
+
return totalBalance;
|
|
124
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ForkChoiceError,
|
|
3
|
+
ForkChoiceErrorCode,
|
|
4
|
+
type InvalidAttestation,
|
|
5
|
+
InvalidAttestationCode,
|
|
6
|
+
type InvalidBlock,
|
|
7
|
+
InvalidBlockCode,
|
|
8
|
+
} from "./forkChoice/errors.js";
|
|
9
|
+
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt, assertValidTerminalPowBlock} from "./forkChoice/forkChoice.js";
|
|
10
|
+
export {
|
|
11
|
+
type AncestorResult,
|
|
12
|
+
AncestorStatus,
|
|
13
|
+
EpochDifference,
|
|
14
|
+
type IForkChoice,
|
|
15
|
+
NotReorgedReason,
|
|
16
|
+
type PowBlockHex,
|
|
17
|
+
} from "./forkChoice/interface.js";
|
|
18
|
+
export {
|
|
19
|
+
type CheckpointWithHex,
|
|
20
|
+
ForkChoiceStore,
|
|
21
|
+
type IForkChoiceStore,
|
|
22
|
+
type JustifiedBalancesGetter,
|
|
23
|
+
} from "./forkChoice/store.js";
|
|
24
|
+
export {type ForkChoiceMetrics, getForkChoiceMetrics} from "./metrics.js";
|
|
25
|
+
export type {
|
|
26
|
+
BlockExtraMeta,
|
|
27
|
+
LVHInvalidResponse,
|
|
28
|
+
LVHValidResponse,
|
|
29
|
+
MaybeValidExecutionStatus,
|
|
30
|
+
ProtoBlock,
|
|
31
|
+
ProtoNode,
|
|
32
|
+
} from "./protoArray/interface.js";
|
|
33
|
+
export {ExecutionStatus} from "./protoArray/interface.js";
|
|
34
|
+
export {ProtoArray} from "./protoArray/protoArray.js";
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {MetricsRegisterExtra} from "@lodestar/utils";
|
|
2
|
+
import {UpdateHeadOpt} from "./forkChoice/forkChoice.js";
|
|
3
|
+
import {NotReorgedReason} from "./forkChoice/interface.js";
|
|
4
|
+
|
|
5
|
+
export type ForkChoiceMetrics = ReturnType<typeof getForkChoiceMetrics>;
|
|
6
|
+
|
|
7
|
+
export function getForkChoiceMetrics(register: MetricsRegisterExtra) {
|
|
8
|
+
return {
|
|
9
|
+
forkChoice: {
|
|
10
|
+
findHead: register.histogram<{caller: string}>({
|
|
11
|
+
name: "beacon_fork_choice_find_head_seconds",
|
|
12
|
+
help: "Time taken to find head in seconds",
|
|
13
|
+
buckets: [0.1, 1, 10],
|
|
14
|
+
labelNames: ["caller"],
|
|
15
|
+
}),
|
|
16
|
+
requests: register.gauge({
|
|
17
|
+
name: "beacon_fork_choice_requests_total",
|
|
18
|
+
help: "Count of occasions where fork choice has tried to find a head",
|
|
19
|
+
}),
|
|
20
|
+
errors: register.gauge<{entrypoint: UpdateHeadOpt}>({
|
|
21
|
+
name: "beacon_fork_choice_errors_total",
|
|
22
|
+
help: "Count of occasions where fork choice has returned an error when trying to find a head",
|
|
23
|
+
labelNames: ["entrypoint"],
|
|
24
|
+
}),
|
|
25
|
+
changedHead: register.gauge({
|
|
26
|
+
name: "beacon_fork_choice_changed_head_total",
|
|
27
|
+
help: "Count of occasions fork choice has found a new head",
|
|
28
|
+
}),
|
|
29
|
+
reorg: register.gauge({
|
|
30
|
+
name: "beacon_fork_choice_reorg_total",
|
|
31
|
+
help: "Count of occasions fork choice has switched to a different chain",
|
|
32
|
+
}),
|
|
33
|
+
reorgDistance: register.histogram({
|
|
34
|
+
name: "beacon_fork_choice_reorg_distance",
|
|
35
|
+
help: "Histogram of re-org distance",
|
|
36
|
+
// We need high resolution in the low range, since re-orgs are a rare but critical event.
|
|
37
|
+
// Add buckets up to 100 to capture high depth re-orgs. Above 100 things are going really bad.
|
|
38
|
+
buckets: [1, 2, 3, 5, 7, 10, 20, 30, 50, 100],
|
|
39
|
+
}),
|
|
40
|
+
votes: register.gauge({
|
|
41
|
+
name: "beacon_fork_choice_votes_count",
|
|
42
|
+
help: "Current count of votes in fork choice data structures",
|
|
43
|
+
}),
|
|
44
|
+
queuedAttestations: register.gauge({
|
|
45
|
+
name: "beacon_fork_choice_queued_attestations_count",
|
|
46
|
+
help: "Count of queued_attestations in fork choice per slot",
|
|
47
|
+
}),
|
|
48
|
+
validatedAttestationDatas: register.gauge({
|
|
49
|
+
name: "beacon_fork_choice_validated_attestation_datas_count",
|
|
50
|
+
help: "Current count of validatedAttestationDatas in fork choice data structures",
|
|
51
|
+
}),
|
|
52
|
+
balancesLength: register.gauge({
|
|
53
|
+
name: "beacon_fork_choice_balances_length",
|
|
54
|
+
help: "Current length of balances in fork choice data structures",
|
|
55
|
+
}),
|
|
56
|
+
nodes: register.gauge({
|
|
57
|
+
name: "beacon_fork_choice_nodes_count",
|
|
58
|
+
help: "Current count of nodes in fork choice data structures",
|
|
59
|
+
}),
|
|
60
|
+
indices: register.gauge({
|
|
61
|
+
name: "beacon_fork_choice_indices_count",
|
|
62
|
+
help: "Current count of indices in fork choice data structures",
|
|
63
|
+
}),
|
|
64
|
+
notReorgedReason: register.counter<{reason: NotReorgedReason}>({
|
|
65
|
+
name: "beacon_fork_choice_not_reorged_reason_total",
|
|
66
|
+
help: "Reason why the current head is not re-orged out",
|
|
67
|
+
labelNames: ["reason"],
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {EffectiveBalanceIncrements} from "@lodestar/state-transition";
|
|
2
|
+
import {ValidatorIndex} from "@lodestar/types";
|
|
3
|
+
import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
|
|
4
|
+
import {VoteTracker} from "./interface.js";
|
|
5
|
+
|
|
6
|
+
// reuse arrays to avoid memory reallocation and gc
|
|
7
|
+
const deltas = new Array<number>();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a list of `deltas`, where there is one delta for each of the indices in `indices`
|
|
11
|
+
*
|
|
12
|
+
* The deltas are formed by a change between `oldBalances` and `newBalances`, and/or a change of vote in `votes`.
|
|
13
|
+
*
|
|
14
|
+
* ## Errors
|
|
15
|
+
*
|
|
16
|
+
* - If a value in `indices` is greater to or equal to `indices.length`.
|
|
17
|
+
*/
|
|
18
|
+
export function computeDeltas(
|
|
19
|
+
numProtoNodes: number,
|
|
20
|
+
votes: VoteTracker[],
|
|
21
|
+
oldBalances: EffectiveBalanceIncrements,
|
|
22
|
+
newBalances: EffectiveBalanceIncrements,
|
|
23
|
+
equivocatingIndices: Set<ValidatorIndex>
|
|
24
|
+
): number[] {
|
|
25
|
+
deltas.length = numProtoNodes;
|
|
26
|
+
deltas.fill(0);
|
|
27
|
+
|
|
28
|
+
// avoid creating new variables in the loop to potentially reduce GC pressure
|
|
29
|
+
let oldBalance: number, newBalance: number;
|
|
30
|
+
let currentIndex: number | null, nextIndex: number | null;
|
|
31
|
+
|
|
32
|
+
for (let vIndex = 0; vIndex < votes.length; vIndex++) {
|
|
33
|
+
const vote = votes[vIndex];
|
|
34
|
+
// There is no need to create a score change if the validator has never voted or both of their
|
|
35
|
+
// votes are for the zero hash (genesis block)
|
|
36
|
+
if (vote === undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
currentIndex = vote.currentIndex;
|
|
40
|
+
nextIndex = vote.nextIndex;
|
|
41
|
+
|
|
42
|
+
// IF the validator was not included in the _old_ balances (i.e. it did not exist yet)
|
|
43
|
+
// then say its balance was 0
|
|
44
|
+
oldBalance = oldBalances[vIndex] ?? 0;
|
|
45
|
+
|
|
46
|
+
// If the validator's vote is not known in the _new_ balances, then use a balance of zero.
|
|
47
|
+
//
|
|
48
|
+
// It is possible that there was a vote for an unknown validator if we change our justified
|
|
49
|
+
// state to a new state with a higher epoch that is on a different fork because that fork may have
|
|
50
|
+
// on-boarded fewer validators than the prior fork.
|
|
51
|
+
newBalance = newBalances === oldBalances ? oldBalance : (newBalances[vIndex] ?? 0);
|
|
52
|
+
|
|
53
|
+
if (equivocatingIndices.size > 0 && equivocatingIndices.has(vIndex)) {
|
|
54
|
+
// this function could be called multiple times but we only want to process slashing validator for 1 time
|
|
55
|
+
if (currentIndex !== null) {
|
|
56
|
+
if (currentIndex >= numProtoNodes) {
|
|
57
|
+
throw new ProtoArrayError({
|
|
58
|
+
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
|
|
59
|
+
index: currentIndex,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
deltas[currentIndex] -= oldBalance;
|
|
63
|
+
}
|
|
64
|
+
vote.currentIndex = null;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (currentIndex !== nextIndex || oldBalance !== newBalance) {
|
|
69
|
+
// We ignore the vote if it is not known in `indices .
|
|
70
|
+
// We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting
|
|
71
|
+
if (currentIndex !== null) {
|
|
72
|
+
if (currentIndex >= numProtoNodes) {
|
|
73
|
+
throw new ProtoArrayError({
|
|
74
|
+
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
|
|
75
|
+
index: currentIndex,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
deltas[currentIndex] -= oldBalance;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// We ignore the vote if it is not known in `indices .
|
|
82
|
+
// We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting
|
|
83
|
+
if (nextIndex !== null) {
|
|
84
|
+
if (nextIndex >= numProtoNodes) {
|
|
85
|
+
throw new ProtoArrayError({
|
|
86
|
+
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
|
|
87
|
+
index: nextIndex,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
deltas[nextIndex] += newBalance;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
vote.currentIndex = nextIndex;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return deltas;
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {Epoch, RootHex} from "@lodestar/types";
|
|
2
|
+
import {LodestarError} from "@lodestar/utils";
|
|
3
|
+
|
|
4
|
+
export enum LVHExecErrorCode {
|
|
5
|
+
PreMergeToInvalid = "PreMergeToInvalid",
|
|
6
|
+
ValidToInvalid = "ValidToInvalid",
|
|
7
|
+
InvalidToValid = "InvalidToValid",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type LVHExecError = {lvhCode: LVHExecErrorCode; blockRoot: RootHex; execHash: RootHex};
|
|
11
|
+
|
|
12
|
+
export enum ProtoArrayErrorCode {
|
|
13
|
+
FINALIZED_NODE_UNKNOWN = "PROTO_ARRAY_ERROR_FINALIZED_NODE_UNKNOWN",
|
|
14
|
+
JUSTIFIED_NODE_UNKNOWN = "PROTO_ARRAY_ERROR_JUSTIFIED_NODE_UNKNOWN",
|
|
15
|
+
INVALID_FINALIZED_ROOT_CHANGE = "PROTO_ARRAY_ERROR_INVALID_FINALIZED_ROOT_CHANGE",
|
|
16
|
+
INVALID_NODE_INDEX = "PROTO_ARRAY_ERROR_INVALID_NODE_INDEX",
|
|
17
|
+
INVALID_PARENT_INDEX = "PROTO_ARRAY_ERROR_INVALID_PARENT_INDEX",
|
|
18
|
+
INVALID_BEST_CHILD_INDEX = "PROTO_ARRAY_ERROR_INVALID_BEST_CHILD_INDEX",
|
|
19
|
+
INVALID_JUSTIFIED_INDEX = "PROTO_ARRAY_ERROR_INVALID_JUSTIFIED_INDEX",
|
|
20
|
+
INVALID_BEST_DESCENDANT_INDEX = "PROTO_ARRAY_ERROR_INVALID_BEST_DESCENDANT_INDEX",
|
|
21
|
+
INVALID_PARENT_DELTA = "PROTO_ARRAY_ERROR_INVALID_PARENT_DELTA",
|
|
22
|
+
INVALID_NODE_DELTA = "PROTO_ARRAY_ERROR_INVALID_NODE_DELTA",
|
|
23
|
+
INDEX_OVERFLOW = "PROTO_ARRAY_ERROR_INDEX_OVERFLOW",
|
|
24
|
+
INVALID_DELTA_LEN = "PROTO_ARRAY_ERROR_INVALID_DELTA_LEN",
|
|
25
|
+
REVERTED_FINALIZED_EPOCH = "PROTO_ARRAY_ERROR_REVERTED_FINALIZED_EPOCH",
|
|
26
|
+
INVALID_BEST_NODE = "PROTO_ARRAY_ERROR_INVALID_BEST_NODE",
|
|
27
|
+
INVALID_BLOCK_EXECUTION_STATUS = "PROTO_ARRAY_INVALID_BLOCK_EXECUTION_STATUS",
|
|
28
|
+
INVALID_JUSTIFIED_EXECUTION_STATUS = "PROTO_ARRAY_INVALID_JUSTIFIED_EXECUTION_STATUS",
|
|
29
|
+
INVALID_LVH_EXECUTION_RESPONSE = "PROTO_ARRAY_INVALID_LVH_EXECUTION_RESPONSE",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ProtoArrayErrorType =
|
|
33
|
+
| {code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN; root: RootHex}
|
|
34
|
+
| {code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN; root: RootHex}
|
|
35
|
+
| {code: ProtoArrayErrorCode.INVALID_FINALIZED_ROOT_CHANGE}
|
|
36
|
+
| {code: ProtoArrayErrorCode.INVALID_NODE_INDEX; index: number}
|
|
37
|
+
| {code: ProtoArrayErrorCode.INVALID_PARENT_INDEX; index: number}
|
|
38
|
+
| {code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX; index: number}
|
|
39
|
+
| {code: ProtoArrayErrorCode.INVALID_JUSTIFIED_INDEX; index: number}
|
|
40
|
+
| {code: ProtoArrayErrorCode.INVALID_BEST_DESCENDANT_INDEX; index: number}
|
|
41
|
+
| {code: ProtoArrayErrorCode.INVALID_PARENT_DELTA; index: number}
|
|
42
|
+
| {code: ProtoArrayErrorCode.INVALID_NODE_DELTA; index: number}
|
|
43
|
+
| {code: ProtoArrayErrorCode.INDEX_OVERFLOW; value: string}
|
|
44
|
+
| {code: ProtoArrayErrorCode.INVALID_DELTA_LEN; deltas: number; indices: number}
|
|
45
|
+
| {code: ProtoArrayErrorCode.REVERTED_FINALIZED_EPOCH; currentFinalizedEpoch: Epoch; newFinalizedEpoch: Epoch}
|
|
46
|
+
| {
|
|
47
|
+
code: ProtoArrayErrorCode.INVALID_BEST_NODE;
|
|
48
|
+
startRoot: RootHex;
|
|
49
|
+
justifiedEpoch: Epoch;
|
|
50
|
+
finalizedEpoch: Epoch;
|
|
51
|
+
headRoot: RootHex;
|
|
52
|
+
headJustifiedEpoch: Epoch;
|
|
53
|
+
headFinalizedEpoch: Epoch;
|
|
54
|
+
}
|
|
55
|
+
| {code: ProtoArrayErrorCode.INVALID_BLOCK_EXECUTION_STATUS; root: RootHex}
|
|
56
|
+
| {code: ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS; root: RootHex}
|
|
57
|
+
| ({code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE} & LVHExecError);
|
|
58
|
+
|
|
59
|
+
export class ProtoArrayError extends LodestarError<ProtoArrayErrorType> {}
|