@lodestar/fork-choice 1.35.0-dev.feed916580 → 1.35.0-rc.1
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.map +1 -0
- package/lib/forkChoice/forkChoice.js +11 -8
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts.map +1 -0
- package/lib/forkChoice/store.d.ts.map +1 -0
- package/lib/index.d.ts.map +1 -0
- 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/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,1651 @@
|
|
|
1
|
+
import {ChainConfig, ChainForkConfig} from "@lodestar/config";
|
|
2
|
+
import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
|
|
3
|
+
import {
|
|
4
|
+
CachedBeaconStateAllForks,
|
|
5
|
+
DataAvailabilityStatus,
|
|
6
|
+
EffectiveBalanceIncrements,
|
|
7
|
+
ZERO_HASH,
|
|
8
|
+
computeEpochAtSlot,
|
|
9
|
+
computeSlotsSinceEpochStart,
|
|
10
|
+
computeStartSlotAtEpoch,
|
|
11
|
+
getAttesterSlashableIndices,
|
|
12
|
+
isExecutionBlockBodyType,
|
|
13
|
+
isExecutionEnabled,
|
|
14
|
+
isExecutionStateType,
|
|
15
|
+
} from "@lodestar/state-transition";
|
|
16
|
+
import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
|
|
17
|
+
import {
|
|
18
|
+
AttesterSlashing,
|
|
19
|
+
BeaconBlock,
|
|
20
|
+
Epoch,
|
|
21
|
+
IndexedAttestation,
|
|
22
|
+
Root,
|
|
23
|
+
RootHex,
|
|
24
|
+
Slot,
|
|
25
|
+
ValidatorIndex,
|
|
26
|
+
bellatrix,
|
|
27
|
+
phase0,
|
|
28
|
+
ssz,
|
|
29
|
+
} from "@lodestar/types";
|
|
30
|
+
import {Logger, MapDef, fromHex, toRootHex} from "@lodestar/utils";
|
|
31
|
+
import {ForkChoiceMetrics} from "../metrics.js";
|
|
32
|
+
import {computeDeltas} from "../protoArray/computeDeltas.js";
|
|
33
|
+
import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js";
|
|
34
|
+
import {
|
|
35
|
+
ExecutionStatus,
|
|
36
|
+
HEX_ZERO_HASH,
|
|
37
|
+
LVHExecResponse,
|
|
38
|
+
MaybeValidExecutionStatus,
|
|
39
|
+
ProtoBlock,
|
|
40
|
+
ProtoNode,
|
|
41
|
+
VoteTracker,
|
|
42
|
+
} from "../protoArray/interface.js";
|
|
43
|
+
import {ProtoArray} from "../protoArray/protoArray.js";
|
|
44
|
+
import {ForkChoiceError, ForkChoiceErrorCode, InvalidAttestationCode, InvalidBlockCode} from "./errors.js";
|
|
45
|
+
import {
|
|
46
|
+
AncestorResult,
|
|
47
|
+
AncestorStatus,
|
|
48
|
+
EpochDifference,
|
|
49
|
+
IForkChoice,
|
|
50
|
+
LatestMessage,
|
|
51
|
+
NotReorgedReason,
|
|
52
|
+
PowBlockHex,
|
|
53
|
+
ShouldOverrideForkChoiceUpdateResult,
|
|
54
|
+
} from "./interface.js";
|
|
55
|
+
import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js";
|
|
56
|
+
|
|
57
|
+
export type ForkChoiceOpts = {
|
|
58
|
+
proposerBoost?: boolean;
|
|
59
|
+
proposerBoostReorg?: boolean;
|
|
60
|
+
computeUnrealized?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export enum UpdateHeadOpt {
|
|
64
|
+
GetCanonicalHead = "getCanonicalHead", // Skip getProposerHead
|
|
65
|
+
GetProposerHead = "getProposerHead", // With getProposerHead
|
|
66
|
+
GetPredictedProposerHead = "getPredictedProposerHead", // With predictProposerHead
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type UpdateAndGetHeadOpt =
|
|
70
|
+
| {mode: UpdateHeadOpt.GetCanonicalHead}
|
|
71
|
+
| {mode: UpdateHeadOpt.GetProposerHead; secFromSlot: number; slot: Slot}
|
|
72
|
+
| {mode: UpdateHeadOpt.GetPredictedProposerHead; secFromSlot: number; slot: Slot};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Provides an implementation of "Ethereum Consensus -- Beacon Chain Fork Choice":
|
|
76
|
+
*
|
|
77
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#fork-choice
|
|
78
|
+
*
|
|
79
|
+
* ## Detail
|
|
80
|
+
*
|
|
81
|
+
* This class wraps `ProtoArray` and provides:
|
|
82
|
+
*
|
|
83
|
+
* - Management of validators latest messages and balances
|
|
84
|
+
* - Management of the justified/finalized checkpoints as seen by fork choice
|
|
85
|
+
* - Queuing of attestations from the current slot
|
|
86
|
+
*
|
|
87
|
+
* This class MUST be used with the following considerations:
|
|
88
|
+
*
|
|
89
|
+
* - Time is not updated automatically, updateTime MUST be called every slot
|
|
90
|
+
*/
|
|
91
|
+
export class ForkChoice implements IForkChoice {
|
|
92
|
+
irrecoverableError?: Error;
|
|
93
|
+
/**
|
|
94
|
+
* Votes currently tracked in the protoArray
|
|
95
|
+
* Indexed by validator index
|
|
96
|
+
* Each vote contains the latest message and previous message
|
|
97
|
+
*/
|
|
98
|
+
private readonly votes: VoteTracker[] = [];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Attestations that arrived at the current slot and must be queued for later processing.
|
|
102
|
+
* NOT currently tracked in the protoArray
|
|
103
|
+
*/
|
|
104
|
+
private readonly queuedAttestations: MapDef<Slot, MapDef<RootHex, Set<ValidatorIndex>>> = new MapDef(
|
|
105
|
+
() => new MapDef(() => new Set())
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* It's inconsistent to count number of queued attestations at different intervals of slot.
|
|
110
|
+
* Instead of that, we count number of queued attestations at the previous slot.
|
|
111
|
+
*/
|
|
112
|
+
private queuedAttestationsPreviousSlot = 0;
|
|
113
|
+
|
|
114
|
+
// Note: as of Jun 2022 Lodestar metrics show that 100% of the times updateHead() is called, synced = false.
|
|
115
|
+
// Because we are processing attestations from gossip, recomputing scores is always necessary
|
|
116
|
+
// /** Avoid having to compute deltas all the times. */
|
|
117
|
+
// private synced = false;
|
|
118
|
+
|
|
119
|
+
/** Cached head */
|
|
120
|
+
private head: ProtoBlock;
|
|
121
|
+
/**
|
|
122
|
+
* Only cache attestation data root hex if it's tree backed since it's available.
|
|
123
|
+
**/
|
|
124
|
+
private validatedAttestationDatas = new Set<string>();
|
|
125
|
+
/** Boost the entire branch with this proposer root as the leaf */
|
|
126
|
+
private proposerBoostRoot: RootHex | null = null;
|
|
127
|
+
/** Score to use in proposer boost, evaluated lazily from justified balances */
|
|
128
|
+
private justifiedProposerBoostScore: number | null = null;
|
|
129
|
+
/** The current effective balances */
|
|
130
|
+
private balances: EffectiveBalanceIncrements;
|
|
131
|
+
/**
|
|
132
|
+
* Instantiates a Fork Choice from some existing components
|
|
133
|
+
*
|
|
134
|
+
* This is useful if the existing components have been loaded from disk after a process restart.
|
|
135
|
+
*/
|
|
136
|
+
constructor(
|
|
137
|
+
private readonly config: ChainForkConfig,
|
|
138
|
+
private readonly fcStore: IForkChoiceStore,
|
|
139
|
+
/** The underlying representation of the block DAG. */
|
|
140
|
+
private readonly protoArray: ProtoArray,
|
|
141
|
+
readonly metrics: ForkChoiceMetrics | null,
|
|
142
|
+
private readonly opts?: ForkChoiceOpts,
|
|
143
|
+
private readonly logger?: Logger
|
|
144
|
+
) {
|
|
145
|
+
this.head = this.updateHead();
|
|
146
|
+
this.balances = this.fcStore.justified.balances;
|
|
147
|
+
|
|
148
|
+
metrics?.forkChoice.votes.addCollect(() => {
|
|
149
|
+
metrics.forkChoice.votes.set(this.votes.length);
|
|
150
|
+
metrics.forkChoice.queuedAttestations.set(this.queuedAttestationsPreviousSlot);
|
|
151
|
+
metrics.forkChoice.validatedAttestationDatas.set(this.validatedAttestationDatas.size);
|
|
152
|
+
metrics.forkChoice.balancesLength.set(this.balances.length);
|
|
153
|
+
metrics.forkChoice.nodes.set(this.protoArray.nodes.length);
|
|
154
|
+
metrics.forkChoice.indices.set(this.protoArray.indices.size);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the block root of an ancestor of `blockRoot` at the given `slot`.
|
|
160
|
+
* (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
|
|
161
|
+
*
|
|
162
|
+
* NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
|
|
163
|
+
*
|
|
164
|
+
* ### Specification
|
|
165
|
+
*
|
|
166
|
+
* Equivalent to:
|
|
167
|
+
*
|
|
168
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
|
|
169
|
+
*/
|
|
170
|
+
getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex {
|
|
171
|
+
return this.protoArray.getAncestor(blockRoot, ancestorSlot);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the cached head root
|
|
176
|
+
*/
|
|
177
|
+
getHeadRoot(): RootHex {
|
|
178
|
+
return this.getHead().blockRoot;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the cached head
|
|
183
|
+
*/
|
|
184
|
+
getHead(): ProtoBlock {
|
|
185
|
+
return this.head;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
*
|
|
190
|
+
* A multiplexer to wrap around the traditional `updateHead()` according to the scenario
|
|
191
|
+
* Scenarios as follow:
|
|
192
|
+
* Prepare to propose in the next slot: getHead() -> predictProposerHead()
|
|
193
|
+
* Proposing in the current slot: updateHead() -> getProposerHead()
|
|
194
|
+
* Others eg. initializing forkchoice, importBlock: updateHead()
|
|
195
|
+
*
|
|
196
|
+
* Only `GetProposerHead` returns additional field `isHeadTimely` and `notReorgedReason` for metrics purpose
|
|
197
|
+
*/
|
|
198
|
+
updateAndGetHead(opt: UpdateAndGetHeadOpt): {
|
|
199
|
+
head: ProtoBlock;
|
|
200
|
+
isHeadTimely?: boolean;
|
|
201
|
+
notReorgedReason?: NotReorgedReason;
|
|
202
|
+
} {
|
|
203
|
+
const {mode} = opt;
|
|
204
|
+
|
|
205
|
+
const canonicalHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead();
|
|
206
|
+
switch (mode) {
|
|
207
|
+
case UpdateHeadOpt.GetPredictedProposerHead:
|
|
208
|
+
return {head: this.predictProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot)};
|
|
209
|
+
case UpdateHeadOpt.GetProposerHead: {
|
|
210
|
+
const {
|
|
211
|
+
proposerHead: head,
|
|
212
|
+
isHeadTimely,
|
|
213
|
+
notReorgedReason,
|
|
214
|
+
} = this.getProposerHead(canonicalHeadBlock, opt.secFromSlot, opt.slot);
|
|
215
|
+
return {head, isHeadTimely, notReorgedReason};
|
|
216
|
+
}
|
|
217
|
+
case UpdateHeadOpt.GetCanonicalHead:
|
|
218
|
+
return {head: canonicalHeadBlock};
|
|
219
|
+
default:
|
|
220
|
+
return {head: canonicalHeadBlock};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Called by `predictProposerHead` and `importBlock`. If the result is not same as blockRoot's block, return true else false
|
|
225
|
+
// See https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/bellatrix/fork-choice.md#should_override_forkchoice_update
|
|
226
|
+
// Return true if the given block passes all criteria to be re-orged out
|
|
227
|
+
// Return false otherwise.
|
|
228
|
+
// Note when proposer boost reorg is disabled, it always returns false
|
|
229
|
+
shouldOverrideForkChoiceUpdate(
|
|
230
|
+
blockRoot: RootHex,
|
|
231
|
+
secFromSlot: number,
|
|
232
|
+
currentSlot: Slot
|
|
233
|
+
): ShouldOverrideForkChoiceUpdateResult {
|
|
234
|
+
const headBlock = this.getBlockHex(blockRoot);
|
|
235
|
+
if (headBlock === null) {
|
|
236
|
+
// should not happen because this block just got imported. Fall back to no-reorg.
|
|
237
|
+
return {shouldOverrideFcu: false, reason: NotReorgedReason.HeadBlockNotAvailable};
|
|
238
|
+
}
|
|
239
|
+
const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
|
|
240
|
+
// Skip re-org attempt if proposer boost (reorg) are disabled
|
|
241
|
+
if (!proposerBoost || !proposerBoostReorg) {
|
|
242
|
+
this.logger?.verbose("Skip shouldOverrideForkChoiceUpdate check since the related flags are disabled", {
|
|
243
|
+
slot: currentSlot,
|
|
244
|
+
proposerBoost,
|
|
245
|
+
proposerBoostReorg,
|
|
246
|
+
});
|
|
247
|
+
return {shouldOverrideFcu: false, reason: NotReorgedReason.ProposerBoostReorgDisabled};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
|
|
251
|
+
const proposalSlot = headBlock.slot + 1;
|
|
252
|
+
|
|
253
|
+
// No reorg if parentBlock isn't available
|
|
254
|
+
if (parentBlock === undefined) {
|
|
255
|
+
return {shouldOverrideFcu: false, reason: NotReorgedReason.ParentBlockNotAvailable};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(
|
|
259
|
+
headBlock,
|
|
260
|
+
parentBlock,
|
|
261
|
+
proposalSlot
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (prelimProposerHead === headBlock) {
|
|
265
|
+
return {shouldOverrideFcu: false, reason: prelimNotReorgedReason ?? NotReorgedReason.Unknown};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const currentTimeOk =
|
|
269
|
+
headBlock.slot === currentSlot ||
|
|
270
|
+
(proposalSlot === currentSlot && this.isProposingOnTime(secFromSlot, currentSlot));
|
|
271
|
+
if (!currentTimeOk) {
|
|
272
|
+
return {shouldOverrideFcu: false, reason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.logger?.verbose("Block is weak. Should override forkchoice update", {blockRoot, slot: currentSlot});
|
|
276
|
+
return {shouldOverrideFcu: true, parentBlock};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the proposer boost root
|
|
281
|
+
*/
|
|
282
|
+
getProposerBoostRoot(): RootHex {
|
|
283
|
+
return this.proposerBoostRoot ?? HEX_ZERO_HASH;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* To predict the proposer head of the next slot. That is, to predict if proposer-boost-reorg could happen.
|
|
288
|
+
* Reason why we can't be certain is because information of the head block is not fully available yet
|
|
289
|
+
* since the current slot hasn't ended especially the attesters' votes.
|
|
290
|
+
*
|
|
291
|
+
* There is a chance we mispredict.
|
|
292
|
+
*
|
|
293
|
+
* By calling this function, we assume we are the proposer of next slot
|
|
294
|
+
*
|
|
295
|
+
*/
|
|
296
|
+
predictProposerHead(headBlock: ProtoBlock, secFromSlot: number, currentSlot: Slot): ProtoBlock {
|
|
297
|
+
const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
|
|
298
|
+
// Skip re-org attempt if proposer boost (reorg) are disabled
|
|
299
|
+
if (!proposerBoost || !proposerBoostReorg) {
|
|
300
|
+
this.logger?.verbose("No proposer boost reorg prediction since the related flags are disabled", {
|
|
301
|
+
slot: currentSlot,
|
|
302
|
+
proposerBoost,
|
|
303
|
+
proposerBoostReorg,
|
|
304
|
+
});
|
|
305
|
+
return headBlock;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const blockRoot = headBlock.blockRoot;
|
|
309
|
+
const result = this.shouldOverrideForkChoiceUpdate(blockRoot, secFromSlot, currentSlot);
|
|
310
|
+
|
|
311
|
+
if (result.shouldOverrideFcu) {
|
|
312
|
+
this.logger?.verbose("Current head is weak. Predicting next block to be built on parent of head.", {
|
|
313
|
+
slot: currentSlot,
|
|
314
|
+
proposerHead: result.parentBlock.blockRoot,
|
|
315
|
+
weakHead: blockRoot,
|
|
316
|
+
});
|
|
317
|
+
return result.parentBlock;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.logger?.verbose("Current head is strong. Predicting next block to be built on head", {
|
|
321
|
+
slot: currentSlot,
|
|
322
|
+
head: headBlock.blockRoot,
|
|
323
|
+
reason: result.reason,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return headBlock;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
*
|
|
331
|
+
* This function takes in the canonical head block and determine the proposer head (canonical head block or its parent)
|
|
332
|
+
* https://github.com/ethereum/consensus-specs/pull/3034 for info about proposer boost reorg
|
|
333
|
+
* This function should only be called during block proposal and only be called after `updateHead()` in `updateAndGetHead()`
|
|
334
|
+
*
|
|
335
|
+
* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head
|
|
336
|
+
*/
|
|
337
|
+
getProposerHead(
|
|
338
|
+
headBlock: ProtoBlock,
|
|
339
|
+
secFromSlot: number,
|
|
340
|
+
slot: Slot
|
|
341
|
+
): {proposerHead: ProtoBlock; isHeadTimely: boolean; notReorgedReason?: NotReorgedReason} {
|
|
342
|
+
const isHeadTimely = headBlock.timeliness;
|
|
343
|
+
let proposerHead = headBlock;
|
|
344
|
+
|
|
345
|
+
// Skip re-org attempt if proposer boost (reorg) are disabled
|
|
346
|
+
const {proposerBoost, proposerBoostReorg} = this.opts ?? {};
|
|
347
|
+
if (!proposerBoost || !proposerBoostReorg) {
|
|
348
|
+
this.logger?.verbose("No proposer boost reorg attempt since the related flags are disabled", {
|
|
349
|
+
slot,
|
|
350
|
+
proposerBoost,
|
|
351
|
+
proposerBoostReorg,
|
|
352
|
+
});
|
|
353
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostReorgDisabled};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const parentBlock = this.protoArray.getBlock(headBlock.parentRoot);
|
|
357
|
+
|
|
358
|
+
// No reorg if parentBlock isn't available
|
|
359
|
+
if (parentBlock === undefined) {
|
|
360
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotAvailable};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const {prelimProposerHead, prelimNotReorgedReason} = this.getPreliminaryProposerHead(headBlock, parentBlock, slot);
|
|
364
|
+
|
|
365
|
+
if (prelimProposerHead === headBlock && prelimNotReorgedReason !== undefined) {
|
|
366
|
+
return {proposerHead, isHeadTimely, notReorgedReason: prelimNotReorgedReason};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Only re-org if we are proposing on-time
|
|
370
|
+
if (!this.isProposingOnTime(secFromSlot, slot)) {
|
|
371
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.NotProposingOnTime};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// No reorg if attempted reorg is more than a single slot
|
|
375
|
+
// Half of single_slot_reorg check in the spec is done in getPreliminaryProposerHead()
|
|
376
|
+
const currentTimeOk = headBlock.slot + 1 === slot;
|
|
377
|
+
if (!currentTimeOk) {
|
|
378
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ReorgMoreThanOneSlot};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// No reorg if proposer boost is still in effect
|
|
382
|
+
const isProposerBoostWornOff = this.proposerBoostRoot !== headBlock.blockRoot;
|
|
383
|
+
if (!isProposerBoostWornOff) {
|
|
384
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ProposerBoostNotWornOff};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// No reorg if headBlock is "not weak" ie. headBlock's weight exceeds (REORG_HEAD_WEIGHT_THRESHOLD = 20)% of total attester weight
|
|
388
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_weak
|
|
389
|
+
const reorgThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
390
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
391
|
+
committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD,
|
|
392
|
+
});
|
|
393
|
+
const headNode = this.protoArray.getNode(headBlock.blockRoot);
|
|
394
|
+
// If headNode is unavailable, give up reorg
|
|
395
|
+
if (headNode === undefined || headNode.weight >= reorgThreshold) {
|
|
396
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.HeadBlockNotWeak};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// No reorg if parentBlock is "not strong" ie. parentBlock's weight is less than or equal to (REORG_PARENT_WEIGHT_THRESHOLD = 160)% of total attester weight
|
|
400
|
+
// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#is_parent_strong
|
|
401
|
+
const parentThreshold = getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
402
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
403
|
+
committeePercent: this.config.REORG_PARENT_WEIGHT_THRESHOLD,
|
|
404
|
+
});
|
|
405
|
+
const parentNode = this.protoArray.getNode(parentBlock.blockRoot);
|
|
406
|
+
// If parentNode is unavailable, give up reorg
|
|
407
|
+
if (parentNode === undefined || parentNode.weight <= parentThreshold) {
|
|
408
|
+
return {proposerHead, isHeadTimely, notReorgedReason: NotReorgedReason.ParentBlockNotStrong};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Reorg if all above checks fail
|
|
412
|
+
this.logger?.verbose("Performing single-slot reorg to remove current weak head", {
|
|
413
|
+
slot,
|
|
414
|
+
proposerHead: parentBlock.blockRoot,
|
|
415
|
+
weakHead: headBlock.blockRoot,
|
|
416
|
+
});
|
|
417
|
+
proposerHead = parentBlock;
|
|
418
|
+
|
|
419
|
+
return {proposerHead, isHeadTimely};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Run the fork choice rule to determine the head.
|
|
424
|
+
* Update the head cache.
|
|
425
|
+
*
|
|
426
|
+
* Very expensive function (400ms / run as of Aug 2021). Call when the head really needs to be re-calculated.
|
|
427
|
+
*
|
|
428
|
+
* ## Specification
|
|
429
|
+
*
|
|
430
|
+
* Is equivalent to:
|
|
431
|
+
*
|
|
432
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_head
|
|
433
|
+
*/
|
|
434
|
+
updateHead(): ProtoBlock {
|
|
435
|
+
// balances is not changed but votes are changed
|
|
436
|
+
|
|
437
|
+
// NOTE: In current Lodestar metrics, 100% of forkChoiceRequests this.synced = false.
|
|
438
|
+
// No need to cache computeDeltas()
|
|
439
|
+
//
|
|
440
|
+
// TODO: In current Lodestar metrics, 100% of forkChoiceRequests result in a changed head.
|
|
441
|
+
// No need to cache the head anymore
|
|
442
|
+
|
|
443
|
+
// Check if scores need to be calculated/updated
|
|
444
|
+
const oldBalances = this.balances;
|
|
445
|
+
const newBalances = this.fcStore.justified.balances;
|
|
446
|
+
const deltas = computeDeltas(
|
|
447
|
+
this.protoArray.nodes.length,
|
|
448
|
+
this.votes,
|
|
449
|
+
oldBalances,
|
|
450
|
+
newBalances,
|
|
451
|
+
this.fcStore.equivocatingIndices
|
|
452
|
+
);
|
|
453
|
+
this.balances = newBalances;
|
|
454
|
+
/**
|
|
455
|
+
* The structure in line with deltas to propagate boost up the branch
|
|
456
|
+
* starting from the proposerIndex
|
|
457
|
+
*/
|
|
458
|
+
let proposerBoost: {root: RootHex; score: number} | null = null;
|
|
459
|
+
if (this.opts?.proposerBoost && this.proposerBoostRoot) {
|
|
460
|
+
const proposerBoostScore =
|
|
461
|
+
this.justifiedProposerBoostScore ??
|
|
462
|
+
getCommitteeFraction(this.fcStore.justified.totalBalance, {
|
|
463
|
+
slotsPerEpoch: SLOTS_PER_EPOCH,
|
|
464
|
+
committeePercent: this.config.PROPOSER_SCORE_BOOST,
|
|
465
|
+
});
|
|
466
|
+
proposerBoost = {root: this.proposerBoostRoot, score: proposerBoostScore};
|
|
467
|
+
this.justifiedProposerBoostScore = proposerBoostScore;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const currentSlot = this.fcStore.currentSlot;
|
|
471
|
+
this.protoArray.applyScoreChanges({
|
|
472
|
+
deltas,
|
|
473
|
+
proposerBoost,
|
|
474
|
+
justifiedEpoch: this.fcStore.justified.checkpoint.epoch,
|
|
475
|
+
justifiedRoot: this.fcStore.justified.checkpoint.rootHex,
|
|
476
|
+
finalizedEpoch: this.fcStore.finalizedCheckpoint.epoch,
|
|
477
|
+
finalizedRoot: this.fcStore.finalizedCheckpoint.rootHex,
|
|
478
|
+
currentSlot,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const headRoot = this.protoArray.findHead(this.fcStore.justified.checkpoint.rootHex, currentSlot);
|
|
482
|
+
const headIndex = this.protoArray.indices.get(headRoot);
|
|
483
|
+
if (headIndex === undefined) {
|
|
484
|
+
throw new ForkChoiceError({
|
|
485
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
486
|
+
root: headRoot,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const headNode = this.protoArray.nodes[headIndex];
|
|
490
|
+
if (headNode === undefined) {
|
|
491
|
+
throw new ForkChoiceError({
|
|
492
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
493
|
+
root: headRoot,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.head = headNode;
|
|
498
|
+
return this.head;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* An iteration over protoArray to get present slots, to be called preemptively
|
|
503
|
+
* from prepareNextSlot to prevent delay on produceBlindedBlock
|
|
504
|
+
* @param windowStart is the slot after which (excluding) to provide present slots
|
|
505
|
+
*/
|
|
506
|
+
getSlotsPresent(windowStart: number): number {
|
|
507
|
+
return this.protoArray.nodes.filter((node) => node.slot > windowStart).length;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Very expensive function, iterates the entire ProtoArray. Called only in debug API */
|
|
511
|
+
getHeads(): ProtoBlock[] {
|
|
512
|
+
return this.protoArray.nodes.filter((node) => node.bestChild === undefined);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** This is for the debug API only */
|
|
516
|
+
getAllNodes(): ProtoNode[] {
|
|
517
|
+
return this.protoArray.nodes;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
getFinalizedCheckpoint(): CheckpointWithHex {
|
|
521
|
+
return this.fcStore.finalizedCheckpoint;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
getJustifiedCheckpoint(): CheckpointWithHex {
|
|
525
|
+
return this.fcStore.justified.checkpoint;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Add `block` to the fork choice DAG.
|
|
530
|
+
*
|
|
531
|
+
* ## Specification
|
|
532
|
+
*
|
|
533
|
+
* Approximates:
|
|
534
|
+
*
|
|
535
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_block
|
|
536
|
+
*
|
|
537
|
+
* It only approximates the specification since it does not run the `state_transition` check.
|
|
538
|
+
* That should have already been called upstream and it's too expensive to call again.
|
|
539
|
+
*
|
|
540
|
+
* ## Notes:
|
|
541
|
+
*
|
|
542
|
+
* The supplied block **must** pass the `state_transition` function as it will not be run here.
|
|
543
|
+
*
|
|
544
|
+
* `justifiedBalances` balances of justified state which is updated synchronously.
|
|
545
|
+
* This ensures that the forkchoice is never out of sync.
|
|
546
|
+
*/
|
|
547
|
+
onBlock(
|
|
548
|
+
block: BeaconBlock,
|
|
549
|
+
state: CachedBeaconStateAllForks,
|
|
550
|
+
blockDelaySec: number,
|
|
551
|
+
currentSlot: Slot,
|
|
552
|
+
executionStatus: MaybeValidExecutionStatus,
|
|
553
|
+
dataAvailabilityStatus: DataAvailabilityStatus
|
|
554
|
+
): ProtoBlock {
|
|
555
|
+
const {parentRoot, slot} = block;
|
|
556
|
+
const parentRootHex = toRootHex(parentRoot);
|
|
557
|
+
// Parent block must be known
|
|
558
|
+
const parentBlock = this.protoArray.getBlock(parentRootHex);
|
|
559
|
+
if (!parentBlock) {
|
|
560
|
+
throw new ForkChoiceError({
|
|
561
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
562
|
+
err: {
|
|
563
|
+
code: InvalidBlockCode.UNKNOWN_PARENT,
|
|
564
|
+
root: parentRootHex,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Blocks cannot be in the future. If they are, their consideration must be delayed until
|
|
570
|
+
// the are in the past.
|
|
571
|
+
//
|
|
572
|
+
// Note: presently, we do not delay consideration. We just drop the block.
|
|
573
|
+
if (slot > this.fcStore.currentSlot) {
|
|
574
|
+
throw new ForkChoiceError({
|
|
575
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
576
|
+
err: {
|
|
577
|
+
code: InvalidBlockCode.FUTURE_SLOT,
|
|
578
|
+
currentSlot: this.fcStore.currentSlot,
|
|
579
|
+
blockSlot: slot,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Check that block is later than the finalized epoch slot (optimization to reduce calls to
|
|
585
|
+
// get_ancestor).
|
|
586
|
+
const finalizedSlot = computeStartSlotAtEpoch(this.fcStore.finalizedCheckpoint.epoch);
|
|
587
|
+
if (slot <= finalizedSlot) {
|
|
588
|
+
throw new ForkChoiceError({
|
|
589
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
590
|
+
err: {
|
|
591
|
+
code: InvalidBlockCode.FINALIZED_SLOT,
|
|
592
|
+
finalizedSlot,
|
|
593
|
+
blockSlot: slot,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check block is a descendant of the finalized block at the checkpoint finalized slot.
|
|
599
|
+
const blockAncestorRoot = this.getAncestor(parentRootHex, finalizedSlot);
|
|
600
|
+
const finalizedRoot = this.fcStore.finalizedCheckpoint.rootHex;
|
|
601
|
+
if (blockAncestorRoot !== finalizedRoot) {
|
|
602
|
+
throw new ForkChoiceError({
|
|
603
|
+
code: ForkChoiceErrorCode.INVALID_BLOCK,
|
|
604
|
+
err: {
|
|
605
|
+
code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT,
|
|
606
|
+
finalizedRoot,
|
|
607
|
+
blockAncestor: blockAncestorRoot,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
|
|
613
|
+
const blockRootHex = toRootHex(blockRoot);
|
|
614
|
+
|
|
615
|
+
// Assign proposer score boost if the block is timely
|
|
616
|
+
// before attesting interval = before 1st interval
|
|
617
|
+
const isTimely = this.isBlockTimely(block, blockDelaySec);
|
|
618
|
+
if (
|
|
619
|
+
this.opts?.proposerBoost &&
|
|
620
|
+
isTimely &&
|
|
621
|
+
// only boost the first block we see
|
|
622
|
+
this.proposerBoostRoot === null
|
|
623
|
+
) {
|
|
624
|
+
this.proposerBoostRoot = blockRootHex;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// As per specs, we should be validating here the terminal conditions of
|
|
628
|
+
// the PoW if this were a merge transition block.
|
|
629
|
+
// (https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/fork-choice.md#on_block)
|
|
630
|
+
//
|
|
631
|
+
// However this check has been moved to the `verifyBlockStateTransition` in
|
|
632
|
+
// `packages/beacon-node/src/chain/blocks/verifyBlock.ts` as:
|
|
633
|
+
//
|
|
634
|
+
// 1. Its prudent to fail fast and not try importing a block in forkChoice.
|
|
635
|
+
// 2. Also the data to run such a validation is readily available there.
|
|
636
|
+
|
|
637
|
+
const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint);
|
|
638
|
+
const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint);
|
|
639
|
+
const stateJustifiedEpoch = justifiedCheckpoint.epoch;
|
|
640
|
+
|
|
641
|
+
// Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if
|
|
642
|
+
// the justified checkpoint changes
|
|
643
|
+
this.updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, () =>
|
|
644
|
+
this.fcStore.justifiedBalancesGetter(justifiedCheckpoint, state)
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const blockEpoch = computeEpochAtSlot(slot);
|
|
648
|
+
|
|
649
|
+
// same logic to compute_pulled_up_tip in the spec, making it inline because of reusing variables
|
|
650
|
+
// If the parent checkpoints are already at the same epoch as the block being imported,
|
|
651
|
+
// it's impossible for the unrealized checkpoints to differ from the parent's. This
|
|
652
|
+
// holds true because:
|
|
653
|
+
//
|
|
654
|
+
// 1. A child block cannot have lower FFG checkpoints than its parent.
|
|
655
|
+
// 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`.
|
|
656
|
+
// 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`.
|
|
657
|
+
//
|
|
658
|
+
// This is an optimization. It should reduce the amount of times we run
|
|
659
|
+
// `process_justification_and_finalization` by approximately 1/3rd when the chain is
|
|
660
|
+
// performing optimally.
|
|
661
|
+
let unrealizedJustifiedCheckpoint: CheckpointWithHex;
|
|
662
|
+
let unrealizedFinalizedCheckpoint: CheckpointWithHex;
|
|
663
|
+
if (this.opts?.computeUnrealized) {
|
|
664
|
+
if (
|
|
665
|
+
parentBlock.unrealizedJustifiedEpoch === blockEpoch &&
|
|
666
|
+
parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch
|
|
667
|
+
) {
|
|
668
|
+
// reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet
|
|
669
|
+
unrealizedJustifiedCheckpoint = {
|
|
670
|
+
epoch: parentBlock.unrealizedJustifiedEpoch,
|
|
671
|
+
root: fromHex(parentBlock.unrealizedJustifiedRoot),
|
|
672
|
+
rootHex: parentBlock.unrealizedJustifiedRoot,
|
|
673
|
+
};
|
|
674
|
+
unrealizedFinalizedCheckpoint = {
|
|
675
|
+
epoch: parentBlock.unrealizedFinalizedEpoch,
|
|
676
|
+
root: fromHex(parentBlock.unrealizedFinalizedRoot),
|
|
677
|
+
rootHex: parentBlock.unrealizedFinalizedRoot,
|
|
678
|
+
};
|
|
679
|
+
} else {
|
|
680
|
+
// compute new, happens 2/3 first blocks of epoch as monitored in mainnet
|
|
681
|
+
const unrealized = computeUnrealizedCheckpoints(state);
|
|
682
|
+
unrealizedJustifiedCheckpoint = toCheckpointWithHex(unrealized.justifiedCheckpoint);
|
|
683
|
+
unrealizedFinalizedCheckpoint = toCheckpointWithHex(unrealized.finalizedCheckpoint);
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
unrealizedJustifiedCheckpoint = justifiedCheckpoint;
|
|
687
|
+
unrealizedFinalizedCheckpoint = finalizedCheckpoint;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Un-realized checkpoints
|
|
691
|
+
// Update best known unrealized justified & finalized checkpoints
|
|
692
|
+
this.updateUnrealizedCheckpoints(unrealizedJustifiedCheckpoint, unrealizedFinalizedCheckpoint, () =>
|
|
693
|
+
this.fcStore.justifiedBalancesGetter(unrealizedJustifiedCheckpoint, state)
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
// If block is from past epochs, try to update store's justified & finalized checkpoints right away
|
|
697
|
+
if (blockEpoch < computeEpochAtSlot(currentSlot)) {
|
|
698
|
+
this.updateCheckpoints(unrealizedJustifiedCheckpoint, unrealizedFinalizedCheckpoint, () =>
|
|
699
|
+
this.fcStore.justifiedBalancesGetter(unrealizedJustifiedCheckpoint, state)
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const targetSlot = computeStartSlotAtEpoch(blockEpoch);
|
|
704
|
+
const targetRoot = slot === targetSlot ? blockRoot : state.blockRoots.get(targetSlot % SLOTS_PER_HISTORICAL_ROOT);
|
|
705
|
+
|
|
706
|
+
// This does not apply a vote to the block, it just makes fork choice aware of the block so
|
|
707
|
+
// it can still be identified as the head even if it doesn't have any votes.
|
|
708
|
+
const protoBlock: ProtoBlock = {
|
|
709
|
+
slot: slot,
|
|
710
|
+
blockRoot: blockRootHex,
|
|
711
|
+
parentRoot: parentRootHex,
|
|
712
|
+
targetRoot: toRootHex(targetRoot),
|
|
713
|
+
stateRoot: toRootHex(block.stateRoot),
|
|
714
|
+
timeliness: isTimely,
|
|
715
|
+
|
|
716
|
+
justifiedEpoch: stateJustifiedEpoch,
|
|
717
|
+
justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root),
|
|
718
|
+
finalizedEpoch: finalizedCheckpoint.epoch,
|
|
719
|
+
finalizedRoot: toRootHex(state.finalizedCheckpoint.root),
|
|
720
|
+
unrealizedJustifiedEpoch: unrealizedJustifiedCheckpoint.epoch,
|
|
721
|
+
unrealizedJustifiedRoot: unrealizedJustifiedCheckpoint.rootHex,
|
|
722
|
+
unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch,
|
|
723
|
+
unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
|
|
724
|
+
|
|
725
|
+
...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
|
|
726
|
+
? {
|
|
727
|
+
executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
|
|
728
|
+
executionPayloadNumber: block.body.executionPayload.blockNumber,
|
|
729
|
+
executionStatus: this.getPostMergeExecStatus(executionStatus),
|
|
730
|
+
dataAvailabilityStatus,
|
|
731
|
+
}
|
|
732
|
+
: {
|
|
733
|
+
executionPayloadBlockHash: null,
|
|
734
|
+
executionStatus: this.getPreMergeExecStatus(executionStatus),
|
|
735
|
+
dataAvailabilityStatus: this.getPreMergeDataStatus(dataAvailabilityStatus),
|
|
736
|
+
}),
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
this.protoArray.onBlock(protoBlock, currentSlot);
|
|
740
|
+
|
|
741
|
+
return protoBlock;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
|
|
746
|
+
*
|
|
747
|
+
* ## Specification
|
|
748
|
+
*
|
|
749
|
+
* Approximates:
|
|
750
|
+
*
|
|
751
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation
|
|
752
|
+
*
|
|
753
|
+
* It only approximates the specification since it does not perform
|
|
754
|
+
* `is_valid_indexed_attestation` since that should already have been called upstream and it's
|
|
755
|
+
* too expensive to call again.
|
|
756
|
+
*
|
|
757
|
+
* ## Notes:
|
|
758
|
+
*
|
|
759
|
+
* The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
|
|
760
|
+
* will not be run here.
|
|
761
|
+
*/
|
|
762
|
+
onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void {
|
|
763
|
+
// Ignore any attestations to the zero hash.
|
|
764
|
+
//
|
|
765
|
+
// This is an edge case that results from the spec aliasing the zero hash to the genesis
|
|
766
|
+
// block. Attesters may attest to the zero hash if they have never seen a block.
|
|
767
|
+
//
|
|
768
|
+
// We have two options here:
|
|
769
|
+
//
|
|
770
|
+
// 1. Apply all zero-hash attestations to the genesis block.
|
|
771
|
+
// 2. Ignore all attestations to the zero hash.
|
|
772
|
+
//
|
|
773
|
+
// (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is
|
|
774
|
+
// fine because votes to the genesis block are not useful; all validators implicitly attest
|
|
775
|
+
// to genesis just by being present in the chain.
|
|
776
|
+
const attestationData = attestation.data;
|
|
777
|
+
const {slot, beaconBlockRoot} = attestationData;
|
|
778
|
+
const blockRootHex = toRootHex(beaconBlockRoot);
|
|
779
|
+
const targetEpoch = attestationData.target.epoch;
|
|
780
|
+
if (ssz.Root.equals(beaconBlockRoot, ZERO_HASH)) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.validateOnAttestation(attestation, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
|
|
785
|
+
|
|
786
|
+
if (slot < this.fcStore.currentSlot) {
|
|
787
|
+
for (const validatorIndex of attestation.attestingIndices) {
|
|
788
|
+
if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
|
|
789
|
+
this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
// The spec declares:
|
|
794
|
+
//
|
|
795
|
+
// ```
|
|
796
|
+
// Attestations can only affect the fork choice of subsequent slots.
|
|
797
|
+
// Delay consideration in the fork choice until their slot is in the past.
|
|
798
|
+
// ```
|
|
799
|
+
const byRoot = this.queuedAttestations.getOrDefault(slot);
|
|
800
|
+
const validatorIndices = byRoot.getOrDefault(blockRootHex);
|
|
801
|
+
for (const validatorIndex of attestation.attestingIndices) {
|
|
802
|
+
if (!this.fcStore.equivocatingIndices.has(validatorIndex)) {
|
|
803
|
+
validatorIndices.add(validatorIndex);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Small different from the spec:
|
|
811
|
+
* We already call is_slashable_attestation_data() and is_valid_indexed_attestation
|
|
812
|
+
* in state transition so no need to do it again
|
|
813
|
+
*/
|
|
814
|
+
onAttesterSlashing(attesterSlashing: AttesterSlashing): void {
|
|
815
|
+
// TODO: we already call in in state-transition, find a way not to recompute it again
|
|
816
|
+
const intersectingIndices = getAttesterSlashableIndices(attesterSlashing);
|
|
817
|
+
for (const validatorIndex of intersectingIndices) {
|
|
818
|
+
this.fcStore.equivocatingIndices.add(validatorIndex);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined {
|
|
823
|
+
const vote = this.votes[validatorIndex];
|
|
824
|
+
if (vote === undefined) {
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
epoch: vote.nextEpoch,
|
|
829
|
+
root: vote.nextIndex === null ? HEX_ZERO_HASH : this.protoArray.nodes[vote.nextIndex].blockRoot,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
|
|
835
|
+
* This should only be called once per slot because:
|
|
836
|
+
* - calling this multiple times in the same slot does not update `votes`
|
|
837
|
+
* - new attestations in the current slot must stay in the queue
|
|
838
|
+
* - new attestations in the old slots are applied to the `votes` already
|
|
839
|
+
* - also side effect of this function is `validatedAttestationDatas` reseted
|
|
840
|
+
*/
|
|
841
|
+
updateTime(currentSlot: Slot): void {
|
|
842
|
+
if (this.fcStore.currentSlot >= currentSlot) return;
|
|
843
|
+
while (this.fcStore.currentSlot < currentSlot) {
|
|
844
|
+
const previousSlot = this.fcStore.currentSlot;
|
|
845
|
+
// Note: we are relying upon `onTick` to update `fcStore.time` to ensure we don't get stuck in a loop.
|
|
846
|
+
this.onTick(previousSlot + 1);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
this.queuedAttestationsPreviousSlot = 0;
|
|
850
|
+
// Process any attestations that might now be eligible.
|
|
851
|
+
this.processAttestationQueue();
|
|
852
|
+
this.validatedAttestationDatas = new Set();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
getTime(): Slot {
|
|
856
|
+
return this.fcStore.currentSlot;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** Returns `true` if the block is known **and** a descendant of the finalized root. */
|
|
860
|
+
hasBlock(blockRoot: Root): boolean {
|
|
861
|
+
return this.hasBlockHex(toRootHex(blockRoot));
|
|
862
|
+
}
|
|
863
|
+
/** Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. */
|
|
864
|
+
getBlock(blockRoot: Root): ProtoBlock | null {
|
|
865
|
+
return this.getBlockHex(toRootHex(blockRoot));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Returns `true` if the block is known **and** a descendant of the finalized root.
|
|
870
|
+
*/
|
|
871
|
+
hasBlockHex(blockRoot: RootHex): boolean {
|
|
872
|
+
const node = this.protoArray.getNode(blockRoot);
|
|
873
|
+
if (node === undefined) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return this.protoArray.isFinalizedRootOrDescendant(node);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Same to hasBlock but without checking if the block is a descendant of the finalized root.
|
|
882
|
+
*/
|
|
883
|
+
hasBlockUnsafe(blockRoot: Root): boolean {
|
|
884
|
+
return this.hasBlockHexUnsafe(toRootHex(blockRoot));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Same to hasBlockHex but without checking if the block is a descendant of the finalized root.
|
|
889
|
+
*/
|
|
890
|
+
hasBlockHexUnsafe(blockRoot: RootHex): boolean {
|
|
891
|
+
return this.protoArray.hasBlock(blockRoot);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
896
|
+
*/
|
|
897
|
+
getBlockHex(blockRoot: RootHex): ProtoBlock | null {
|
|
898
|
+
const node = this.protoArray.getNode(blockRoot);
|
|
899
|
+
if (!node) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!this.protoArray.isFinalizedRootOrDescendant(node)) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
...node,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
getJustifiedBlock(): ProtoBlock {
|
|
913
|
+
const block = this.getBlockHex(this.fcStore.justified.checkpoint.rootHex);
|
|
914
|
+
if (!block) {
|
|
915
|
+
throw new ForkChoiceError({
|
|
916
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
917
|
+
root: this.fcStore.justified.checkpoint.rootHex,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
return block;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
getFinalizedBlock(): ProtoBlock {
|
|
924
|
+
const block = this.getBlockHex(this.fcStore.finalizedCheckpoint.rootHex);
|
|
925
|
+
if (!block) {
|
|
926
|
+
throw new ForkChoiceError({
|
|
927
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
928
|
+
root: this.fcStore.finalizedCheckpoint.rootHex,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return block;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Returns true if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
936
|
+
*
|
|
937
|
+
* Always returns `false` if either input roots are unknown.
|
|
938
|
+
* Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
|
|
939
|
+
*/
|
|
940
|
+
isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
|
|
941
|
+
return this.protoArray.isDescendant(ancestorRoot, descendantRoot);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* All indices in votes are relative to proto array so always keep it up to date
|
|
946
|
+
*/
|
|
947
|
+
prune(finalizedRoot: RootHex): ProtoBlock[] {
|
|
948
|
+
const prunedNodes = this.protoArray.maybePrune(finalizedRoot);
|
|
949
|
+
const prunedCount = prunedNodes.length;
|
|
950
|
+
for (let i = 0; i < this.votes.length; i++) {
|
|
951
|
+
const vote = this.votes[i];
|
|
952
|
+
// validator has never voted
|
|
953
|
+
if (vote === undefined) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (vote.currentIndex !== null) {
|
|
958
|
+
if (vote.currentIndex >= prunedCount) {
|
|
959
|
+
vote.currentIndex -= prunedCount;
|
|
960
|
+
} else {
|
|
961
|
+
// the vote was for a pruned proto node
|
|
962
|
+
vote.currentIndex = null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (vote.nextIndex !== null) {
|
|
967
|
+
if (vote.nextIndex >= prunedCount) {
|
|
968
|
+
vote.nextIndex -= prunedCount;
|
|
969
|
+
} else {
|
|
970
|
+
// the vote was for a pruned proto node
|
|
971
|
+
vote.nextIndex = null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return prunedNodes;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
setPruneThreshold(threshold: number): void {
|
|
979
|
+
this.protoArray.pruneThreshold = threshold;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Iterates backwards through block summaries, starting from a block root.
|
|
984
|
+
* Return only the non-finalized blocks.
|
|
985
|
+
*/
|
|
986
|
+
iterateAncestorBlocks(blockRoot: RootHex): IterableIterator<ProtoBlock> {
|
|
987
|
+
return this.protoArray.iterateAncestorNodes(blockRoot);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Returns all blocks backwards starting from a block root.
|
|
992
|
+
* Return only the non-finalized blocks.
|
|
993
|
+
*/
|
|
994
|
+
getAllAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
|
|
995
|
+
const blocks = this.protoArray.getAllAncestorNodes(blockRoot);
|
|
996
|
+
// the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
|
|
997
|
+
return blocks.slice(0, blocks.length - 1);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
|
|
1002
|
+
*/
|
|
1003
|
+
getAllNonAncestorBlocks(blockRoot: RootHex): ProtoBlock[] {
|
|
1004
|
+
return this.protoArray.getAllNonAncestorNodes(blockRoot);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Returns both ancestor and non-ancestor blocks in a single traversal.
|
|
1009
|
+
*/
|
|
1010
|
+
getAllAncestorAndNonAncestorBlocks(blockRoot: RootHex): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]} {
|
|
1011
|
+
const {ancestors, nonAncestors} = this.protoArray.getAllAncestorAndNonAncestorNodes(blockRoot);
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
// the last node is the previous finalized one, it's there to check onBlock finalized checkpoint only.
|
|
1015
|
+
ancestors: ancestors.slice(0, ancestors.length - 1),
|
|
1016
|
+
nonAncestors,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null {
|
|
1021
|
+
if (slot > this.head.slot) {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (slot === this.head.slot) {
|
|
1026
|
+
return this.head;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
|
|
1030
|
+
if (block.slot === slot) {
|
|
1031
|
+
return block;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null {
|
|
1038
|
+
if (slot >= this.head.slot) {
|
|
1039
|
+
return this.head;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
|
|
1043
|
+
if (slot >= block.slot) {
|
|
1044
|
+
return block;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
|
|
1051
|
+
forwarditerateAncestorBlocks(): ProtoBlock[] {
|
|
1052
|
+
return this.protoArray.nodes;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
*forwardIterateDescendants(blockRoot: RootHex): IterableIterator<ProtoBlock> {
|
|
1056
|
+
const rootsInChain = new Set([blockRoot]);
|
|
1057
|
+
|
|
1058
|
+
const blockIndex = this.protoArray.indices.get(blockRoot);
|
|
1059
|
+
if (blockIndex === undefined) {
|
|
1060
|
+
throw new ForkChoiceError({
|
|
1061
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
1062
|
+
root: blockRoot,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for (let i = blockIndex + 1; i < this.protoArray.nodes.length; i++) {
|
|
1067
|
+
const node = this.protoArray.nodes[i];
|
|
1068
|
+
if (rootsInChain.has(node.parentRoot)) {
|
|
1069
|
+
rootsInChain.add(node.blockRoot);
|
|
1070
|
+
yield node;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
|
|
1076
|
+
getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] {
|
|
1077
|
+
return this.protoArray.nodes.filter((node) => node.parentRoot === parentRoot);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */
|
|
1081
|
+
getBlockSummariesAtSlot(slot: Slot): ProtoBlock[] {
|
|
1082
|
+
const nodes = this.protoArray.nodes;
|
|
1083
|
+
const blocksAtSlot: ProtoBlock[] = [];
|
|
1084
|
+
for (let i = 0, len = nodes.length; i < len; i++) {
|
|
1085
|
+
const node = nodes[i];
|
|
1086
|
+
if (node.slot === slot) {
|
|
1087
|
+
blocksAtSlot.push(node);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return blocksAtSlot;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */
|
|
1094
|
+
getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult {
|
|
1095
|
+
const prevNode = this.protoArray.getNode(prevBlock.blockRoot);
|
|
1096
|
+
const newNode = this.protoArray.getNode(newBlock.blockRoot);
|
|
1097
|
+
if (!prevNode || !newNode) {
|
|
1098
|
+
return {code: AncestorStatus.BlockUnknown};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const commonAncestor = this.protoArray.getCommonAncestor(prevNode, newNode);
|
|
1102
|
+
// No common ancestor, should never happen. Return null to not throw
|
|
1103
|
+
if (!commonAncestor) {
|
|
1104
|
+
return {code: AncestorStatus.NoCommonAncenstor};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// If common node is one of both nodes, then they are direct descendants, return null
|
|
1108
|
+
if (commonAncestor.blockRoot === prevNode.blockRoot || commonAncestor.blockRoot === newNode.blockRoot) {
|
|
1109
|
+
return {code: AncestorStatus.Descendant};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return {code: AncestorStatus.CommonAncestor, depth: Math.max(newNode.slot, prevNode.slot) - commonAncestor.slot};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Optimistic sync validate till validated latest hash, invalidate any descendant
|
|
1117
|
+
* branch if invalidate till hash provided
|
|
1118
|
+
*
|
|
1119
|
+
* Proxies to protoArray's validateLatestHash and could run extra validations for the
|
|
1120
|
+
* justified's status as well as validate the terminal conditions if terminal block
|
|
1121
|
+
* becomes valid
|
|
1122
|
+
*/
|
|
1123
|
+
validateLatestHash(execResponse: LVHExecResponse): void {
|
|
1124
|
+
try {
|
|
1125
|
+
this.protoArray.validateLatestHash(execResponse, this.fcStore.currentSlot);
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
if (e instanceof ProtoArrayError && e.type.code === ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE) {
|
|
1128
|
+
this.irrecoverableError = e;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* A dependent root is the block root of the last block before the state transition that decided a specific shuffling
|
|
1135
|
+
*
|
|
1136
|
+
* For proposer shuffling with 0 epochs of lookahead = previous immediate epoch transition
|
|
1137
|
+
* For attester shuffling with 1 epochs of lookahead = last epoch's epoch transition
|
|
1138
|
+
*
|
|
1139
|
+
* ```
|
|
1140
|
+
* epoch: 0 1 2 3 4
|
|
1141
|
+
* |-------|-------|=======|-------|
|
|
1142
|
+
* dependent root A -------------^
|
|
1143
|
+
* dependent root B -----^
|
|
1144
|
+
* ```
|
|
1145
|
+
* - proposer shuffling for a block in epoch 2: dependent root A (EpochDifference = 0)
|
|
1146
|
+
* - attester shuffling for a block in epoch 2: dependent root B (EpochDifference = 1)
|
|
1147
|
+
*/
|
|
1148
|
+
getDependentRoot(block: ProtoBlock, epochDifference: EpochDifference): RootHex {
|
|
1149
|
+
// The navigation at the end of the while loop will always progress backwards,
|
|
1150
|
+
// jumping to a block with a strictly less slot number. So the condition `blockEpoch < atEpoch`
|
|
1151
|
+
// is guaranteed to happen. Given the use of target blocks for faster navigation, it will take
|
|
1152
|
+
// at most `2 * (blockEpoch - atEpoch + 1)` iterations to find the dependent root.
|
|
1153
|
+
|
|
1154
|
+
const beforeSlot = block.slot - (block.slot % SLOTS_PER_EPOCH) - epochDifference * SLOTS_PER_EPOCH;
|
|
1155
|
+
|
|
1156
|
+
// Special case close to genesis block, return the genesis block root
|
|
1157
|
+
if (beforeSlot <= 0) {
|
|
1158
|
+
const genesisBlock = this.protoArray.nodes[0];
|
|
1159
|
+
if (genesisBlock === undefined || genesisBlock.slot !== 0) {
|
|
1160
|
+
throw Error("Genesis block not available");
|
|
1161
|
+
}
|
|
1162
|
+
return genesisBlock.blockRoot;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const finalizedSlot = this.getFinalizedBlock().slot;
|
|
1166
|
+
while (block.slot >= finalizedSlot) {
|
|
1167
|
+
// Dependant root must be in epoch less than `beforeSlot`
|
|
1168
|
+
if (block.slot < beforeSlot) {
|
|
1169
|
+
return block.blockRoot;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Skip one last jump if there's no skipped slot at first slot of the epoch
|
|
1173
|
+
if (block.slot === beforeSlot) {
|
|
1174
|
+
return block.parentRoot;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
block =
|
|
1178
|
+
block.blockRoot === block.targetRoot
|
|
1179
|
+
? // For the first slot of the epoch, a block is it's own target
|
|
1180
|
+
this.protoArray.getBlockReadonly(block.parentRoot)
|
|
1181
|
+
: // else we can navigate much faster jumping to the target block
|
|
1182
|
+
this.protoArray.getBlockReadonly(block.targetRoot);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
throw Error(`Not found dependent root for block slot ${block.slot}, epoch difference ${epochDifference}`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Return true if the block is timely for the current slot.
|
|
1190
|
+
* Child class can overwrite this for testing purpose.
|
|
1191
|
+
*/
|
|
1192
|
+
protected isBlockTimely(block: BeaconBlock, blockDelaySec: number): boolean {
|
|
1193
|
+
const fork = this.config.getForkName(block.slot);
|
|
1194
|
+
const isBeforeLateBlockCutoff = blockDelaySec * 1000 < this.config.getAttestationDueMs(fork);
|
|
1195
|
+
return this.fcStore.currentSlot === block.slot && isBeforeLateBlockCutoff;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/phase0/fork-choice.md#is_proposing_on_time
|
|
1200
|
+
*/
|
|
1201
|
+
private isProposingOnTime(secFromSlot: number, slot: Slot): boolean {
|
|
1202
|
+
const fork = this.config.getForkName(slot);
|
|
1203
|
+
const proposerReorgCutoff = this.config.getProposerReorgCutoffMs(fork);
|
|
1204
|
+
return secFromSlot * 1000 <= proposerReorgCutoff;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge {
|
|
1208
|
+
if (executionStatus !== ExecutionStatus.PreMerge)
|
|
1209
|
+
throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`);
|
|
1210
|
+
return executionStatus;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private getPreMergeDataStatus(dataAvailabilityStatus: DataAvailabilityStatus): DataAvailabilityStatus.PreData {
|
|
1214
|
+
if (dataAvailabilityStatus !== DataAvailabilityStatus.PreData)
|
|
1215
|
+
throw Error(
|
|
1216
|
+
`Invalid pre-merge data status: expected: ${DataAvailabilityStatus.PreData}, got ${dataAvailabilityStatus}`
|
|
1217
|
+
);
|
|
1218
|
+
return dataAvailabilityStatus;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private getPostMergeExecStatus(
|
|
1222
|
+
executionStatus: MaybeValidExecutionStatus
|
|
1223
|
+
): ExecutionStatus.Valid | ExecutionStatus.Syncing {
|
|
1224
|
+
if (executionStatus === ExecutionStatus.PreMerge)
|
|
1225
|
+
throw Error(
|
|
1226
|
+
`Invalid post-merge execution status: expected: ${ExecutionStatus.Syncing} or ${ExecutionStatus.Valid} , got ${executionStatus}`
|
|
1227
|
+
);
|
|
1228
|
+
return executionStatus;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Why `getJustifiedBalances` getter?
|
|
1233
|
+
* - updateCheckpoints() is called in both on_block and on_tick.
|
|
1234
|
+
* - Our cache strategy to get justified balances is incomplete, it can't regen all possible states.
|
|
1235
|
+
* - If the justified state is not available it will get one that is "closest" to the justified checkpoint.
|
|
1236
|
+
* - As a last resort fallback the state that references the new justified checkpoint is close or equal to the
|
|
1237
|
+
* desired justified state. However, the state is available only in the on_block handler
|
|
1238
|
+
* - `getJustifiedBalances` makes the dynamics of justified balances cache easier to reason about
|
|
1239
|
+
*
|
|
1240
|
+
* **`on_block`**:
|
|
1241
|
+
* May need the justified balances of:
|
|
1242
|
+
* - justifiedCheckpoint
|
|
1243
|
+
* - unrealizedJustifiedCheckpoint
|
|
1244
|
+
* These balances are not immediately available so the getter calls a cache fn `() => cache.getBalances()`
|
|
1245
|
+
*
|
|
1246
|
+
* **`on_tick`**
|
|
1247
|
+
* May need the justified balances of:
|
|
1248
|
+
* - unrealizedJustified: Already available in `CheckpointHexWithBalance`
|
|
1249
|
+
* Since this balances are already available the getter is just `() => balances`, without cache interaction
|
|
1250
|
+
*/
|
|
1251
|
+
private updateCheckpoints(
|
|
1252
|
+
justifiedCheckpoint: CheckpointWithHex,
|
|
1253
|
+
finalizedCheckpoint: CheckpointWithHex,
|
|
1254
|
+
getJustifiedBalances: () => JustifiedBalances
|
|
1255
|
+
): void {
|
|
1256
|
+
// Update justified checkpoint.
|
|
1257
|
+
if (justifiedCheckpoint.epoch > this.fcStore.justified.checkpoint.epoch) {
|
|
1258
|
+
this.fcStore.justified = {checkpoint: justifiedCheckpoint, balances: getJustifiedBalances()};
|
|
1259
|
+
this.justifiedProposerBoostScore = null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Update finalized checkpoint.
|
|
1263
|
+
if (finalizedCheckpoint.epoch > this.fcStore.finalizedCheckpoint.epoch) {
|
|
1264
|
+
this.fcStore.finalizedCheckpoint = finalizedCheckpoint;
|
|
1265
|
+
this.justifiedProposerBoostScore = null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Update unrealized checkpoints in store if necessary
|
|
1271
|
+
*/
|
|
1272
|
+
private updateUnrealizedCheckpoints(
|
|
1273
|
+
unrealizedJustifiedCheckpoint: CheckpointWithHex,
|
|
1274
|
+
unrealizedFinalizedCheckpoint: CheckpointWithHex,
|
|
1275
|
+
getJustifiedBalances: () => JustifiedBalances
|
|
1276
|
+
): void {
|
|
1277
|
+
if (unrealizedJustifiedCheckpoint.epoch > this.fcStore.unrealizedJustified.checkpoint.epoch) {
|
|
1278
|
+
this.fcStore.unrealizedJustified = {
|
|
1279
|
+
checkpoint: unrealizedJustifiedCheckpoint,
|
|
1280
|
+
balances: getJustifiedBalances(),
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (unrealizedFinalizedCheckpoint.epoch > this.fcStore.unrealizedFinalizedCheckpoint.epoch) {
|
|
1284
|
+
this.fcStore.unrealizedFinalizedCheckpoint = unrealizedFinalizedCheckpoint;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Validates the `indexed_attestation` for application to fork choice.
|
|
1290
|
+
*
|
|
1291
|
+
* ## Specification
|
|
1292
|
+
*
|
|
1293
|
+
* Equivalent to:
|
|
1294
|
+
*
|
|
1295
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#validate_on_attestation
|
|
1296
|
+
*/
|
|
1297
|
+
private validateOnAttestation(
|
|
1298
|
+
indexedAttestation: IndexedAttestation,
|
|
1299
|
+
slot: Slot,
|
|
1300
|
+
blockRootHex: string,
|
|
1301
|
+
targetEpoch: Epoch,
|
|
1302
|
+
attDataRoot: string,
|
|
1303
|
+
// forceImport attestation even if too old, mostly used in spec tests
|
|
1304
|
+
forceImport?: boolean
|
|
1305
|
+
): void {
|
|
1306
|
+
// There is no point in processing an attestation with an empty bitfield. Reject
|
|
1307
|
+
// it immediately.
|
|
1308
|
+
//
|
|
1309
|
+
// This is not in the specification, however it should be transparent to other nodes. We
|
|
1310
|
+
// return early here to avoid wasting precious resources verifying the rest of it.
|
|
1311
|
+
if (!indexedAttestation.attestingIndices.length) {
|
|
1312
|
+
throw new ForkChoiceError({
|
|
1313
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1314
|
+
err: {
|
|
1315
|
+
code: InvalidAttestationCode.EMPTY_AGGREGATION_BITFIELD,
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (!this.validatedAttestationDatas.has(attDataRoot)) {
|
|
1321
|
+
this.validateAttestationData(indexedAttestation.data, slot, blockRootHex, targetEpoch, attDataRoot, forceImport);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private validateAttestationData(
|
|
1326
|
+
attestationData: phase0.AttestationData,
|
|
1327
|
+
slot: Slot,
|
|
1328
|
+
beaconBlockRootHex: string,
|
|
1329
|
+
targetEpoch: Epoch,
|
|
1330
|
+
attDataRoot: string,
|
|
1331
|
+
// forceImport attestation even if too old, mostly used in spec tests
|
|
1332
|
+
forceImport?: boolean
|
|
1333
|
+
): void {
|
|
1334
|
+
const epochNow = computeEpochAtSlot(this.fcStore.currentSlot);
|
|
1335
|
+
const targetRootHex = toRootHex(attestationData.target.root);
|
|
1336
|
+
|
|
1337
|
+
// Attestation must be from the current of previous epoch.
|
|
1338
|
+
if (targetEpoch > epochNow) {
|
|
1339
|
+
throw new ForkChoiceError({
|
|
1340
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1341
|
+
err: {
|
|
1342
|
+
code: InvalidAttestationCode.FUTURE_EPOCH,
|
|
1343
|
+
attestationEpoch: targetEpoch,
|
|
1344
|
+
currentEpoch: epochNow,
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (!forceImport && targetEpoch + 1 < epochNow) {
|
|
1350
|
+
throw new ForkChoiceError({
|
|
1351
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1352
|
+
err: {
|
|
1353
|
+
code: InvalidAttestationCode.PAST_EPOCH,
|
|
1354
|
+
attestationEpoch: targetEpoch,
|
|
1355
|
+
currentEpoch: epochNow,
|
|
1356
|
+
},
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (targetEpoch !== computeEpochAtSlot(slot)) {
|
|
1361
|
+
throw new ForkChoiceError({
|
|
1362
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1363
|
+
err: {
|
|
1364
|
+
code: InvalidAttestationCode.BAD_TARGET_EPOCH,
|
|
1365
|
+
target: targetEpoch,
|
|
1366
|
+
slot,
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Attestation target must be for a known block.
|
|
1372
|
+
//
|
|
1373
|
+
// We do not delay the block for later processing to reduce complexity and DoS attack
|
|
1374
|
+
// surface.
|
|
1375
|
+
if (!this.protoArray.hasBlock(targetRootHex)) {
|
|
1376
|
+
throw new ForkChoiceError({
|
|
1377
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1378
|
+
err: {
|
|
1379
|
+
code: InvalidAttestationCode.UNKNOWN_TARGET_ROOT,
|
|
1380
|
+
root: targetRootHex,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Load the block for `attestation.data.beacon_block_root`.
|
|
1386
|
+
//
|
|
1387
|
+
// This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork
|
|
1388
|
+
// choice. Any known, non-finalized block should be in fork choice, so this check
|
|
1389
|
+
// immediately filters out attestations that attest to a block that has not been processed.
|
|
1390
|
+
//
|
|
1391
|
+
// Attestations must be for a known block. If the block is unknown, we simply drop the
|
|
1392
|
+
// attestation and do not delay consideration for later.
|
|
1393
|
+
const block = this.protoArray.getBlock(beaconBlockRootHex);
|
|
1394
|
+
if (!block) {
|
|
1395
|
+
throw new ForkChoiceError({
|
|
1396
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1397
|
+
err: {
|
|
1398
|
+
code: InvalidAttestationCode.UNKNOWN_HEAD_BLOCK,
|
|
1399
|
+
beaconBlockRoot: beaconBlockRootHex,
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// If an attestation points to a block that is from an earlier slot than the attestation,
|
|
1405
|
+
// then all slots between the block and attestation must be skipped. Therefore if the block
|
|
1406
|
+
// is from a prior epoch to the attestation, then the target root must be equal to the root
|
|
1407
|
+
// of the block that is being attested to.
|
|
1408
|
+
const expectedTargetHex = targetEpoch > computeEpochAtSlot(block.slot) ? beaconBlockRootHex : block.targetRoot;
|
|
1409
|
+
|
|
1410
|
+
if (expectedTargetHex !== targetRootHex) {
|
|
1411
|
+
throw new ForkChoiceError({
|
|
1412
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1413
|
+
err: {
|
|
1414
|
+
code: InvalidAttestationCode.INVALID_TARGET,
|
|
1415
|
+
attestation: targetRootHex,
|
|
1416
|
+
local: expectedTargetHex,
|
|
1417
|
+
},
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Attestations must not be for blocks in the future. If this is the case, the attestation
|
|
1422
|
+
// should not be considered.
|
|
1423
|
+
if (block.slot > slot) {
|
|
1424
|
+
throw new ForkChoiceError({
|
|
1425
|
+
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
|
|
1426
|
+
err: {
|
|
1427
|
+
code: InvalidAttestationCode.ATTESTS_TO_FUTURE_BLOCK,
|
|
1428
|
+
block: block.slot,
|
|
1429
|
+
attestation: slot,
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
this.validatedAttestationDatas.add(attDataRoot);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Add a validator's latest message to the tracked votes
|
|
1439
|
+
*/
|
|
1440
|
+
private addLatestMessage(validatorIndex: ValidatorIndex, nextEpoch: Epoch, nextRoot: RootHex): void {
|
|
1441
|
+
const vote = this.votes[validatorIndex];
|
|
1442
|
+
// should not happen, attestation is validated before this step
|
|
1443
|
+
const nextIndex = this.protoArray.indices.get(nextRoot);
|
|
1444
|
+
if (nextIndex === undefined) {
|
|
1445
|
+
throw new Error(`Could not find proto index for nextRoot ${nextRoot}`);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (vote === undefined) {
|
|
1449
|
+
this.votes[validatorIndex] = {
|
|
1450
|
+
currentIndex: null,
|
|
1451
|
+
nextIndex,
|
|
1452
|
+
nextEpoch,
|
|
1453
|
+
};
|
|
1454
|
+
} else if (nextEpoch > vote.nextEpoch) {
|
|
1455
|
+
vote.nextIndex = nextIndex;
|
|
1456
|
+
vote.nextEpoch = nextEpoch;
|
|
1457
|
+
}
|
|
1458
|
+
// else its an old vote, don't count it
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Processes and removes from the queue any queued attestations which may now be eligible for
|
|
1463
|
+
* processing due to the slot clock incrementing.
|
|
1464
|
+
*/
|
|
1465
|
+
private processAttestationQueue(): void {
|
|
1466
|
+
const currentSlot = this.fcStore.currentSlot;
|
|
1467
|
+
for (const [slot, byRoot] of this.queuedAttestations.entries()) {
|
|
1468
|
+
const targetEpoch = computeEpochAtSlot(slot);
|
|
1469
|
+
if (slot < currentSlot) {
|
|
1470
|
+
this.queuedAttestations.delete(slot);
|
|
1471
|
+
for (const [blockRoot, validatorIndices] of byRoot.entries()) {
|
|
1472
|
+
const blockRootHex = blockRoot;
|
|
1473
|
+
for (const validatorIndex of validatorIndices) {
|
|
1474
|
+
// equivocatingIndices was checked in onAttestation
|
|
1475
|
+
this.addLatestMessage(validatorIndex, targetEpoch, blockRootHex);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (slot === currentSlot - 1) {
|
|
1479
|
+
this.queuedAttestationsPreviousSlot += validatorIndices.size;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Called whenever the current time increases.
|
|
1490
|
+
*
|
|
1491
|
+
* ## Specification
|
|
1492
|
+
*
|
|
1493
|
+
* Equivalent to:
|
|
1494
|
+
*
|
|
1495
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_tick
|
|
1496
|
+
*/
|
|
1497
|
+
private onTick(time: Slot): void {
|
|
1498
|
+
const previousSlot = this.fcStore.currentSlot;
|
|
1499
|
+
|
|
1500
|
+
if (time > previousSlot + 1) {
|
|
1501
|
+
throw new ForkChoiceError({
|
|
1502
|
+
code: ForkChoiceErrorCode.INCONSISTENT_ON_TICK,
|
|
1503
|
+
previousSlot,
|
|
1504
|
+
time,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Update store time
|
|
1509
|
+
this.fcStore.currentSlot = time;
|
|
1510
|
+
// Reset proposer boost if this is a new slot.
|
|
1511
|
+
if (this.proposerBoostRoot) {
|
|
1512
|
+
// Since previous weight was boosted, we need would now need to recalculate the scores without the boost
|
|
1513
|
+
this.proposerBoostRoot = null;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Not a new epoch, return.
|
|
1517
|
+
if (computeSlotsSinceEpochStart(time) !== 0) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// If a new epoch, pull-up justification and finalization from previous epoch
|
|
1522
|
+
this.updateCheckpoints(
|
|
1523
|
+
this.fcStore.unrealizedJustified.checkpoint,
|
|
1524
|
+
this.fcStore.unrealizedFinalizedCheckpoint,
|
|
1525
|
+
() => this.fcStore.unrealizedJustified.balances
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
*
|
|
1531
|
+
* Common logic of get_proposer_head() and should_override_forkchoice_update()
|
|
1532
|
+
* No one should be calling this function except these two
|
|
1533
|
+
*
|
|
1534
|
+
*/
|
|
1535
|
+
private getPreliminaryProposerHead(
|
|
1536
|
+
headBlock: ProtoBlock,
|
|
1537
|
+
parentBlock: ProtoBlock,
|
|
1538
|
+
slot: Slot
|
|
1539
|
+
): {prelimProposerHead: ProtoBlock; prelimNotReorgedReason?: NotReorgedReason} {
|
|
1540
|
+
let prelimProposerHead = headBlock;
|
|
1541
|
+
// No reorg if headBlock is on time
|
|
1542
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_late
|
|
1543
|
+
const isHeadLate = !headBlock.timeliness;
|
|
1544
|
+
if (!isHeadLate) {
|
|
1545
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// No reorg if we are at epoch boundary where proposer shuffling could change
|
|
1549
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_shuffling_stable
|
|
1550
|
+
const isShufflingStable = slot % SLOTS_PER_EPOCH !== 0;
|
|
1551
|
+
if (!isShufflingStable) {
|
|
1552
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotShufflingStable};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// No reorg if headBlock and parentBlock are not ffg competitive
|
|
1556
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_ffg_competitive
|
|
1557
|
+
const {unrealizedJustifiedEpoch: headBlockCpEpoch, unrealizedJustifiedRoot: headBlockCpRoot} = headBlock;
|
|
1558
|
+
const {unrealizedJustifiedEpoch: parentBlockCpEpoch, unrealizedJustifiedRoot: parentBlockCpRoot} = parentBlock;
|
|
1559
|
+
const isFFGCompetitive = headBlockCpEpoch === parentBlockCpEpoch && headBlockCpRoot === parentBlockCpRoot;
|
|
1560
|
+
if (!isFFGCompetitive) {
|
|
1561
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.NotFFGCompetitive};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// No reorg if chain is not finalizing within REORG_MAX_EPOCHS_SINCE_FINALIZATION
|
|
1565
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_finalization_ok
|
|
1566
|
+
const epochsSinceFinalization = computeEpochAtSlot(slot) - this.getFinalizedCheckpoint().epoch;
|
|
1567
|
+
const isFinalizationOk = epochsSinceFinalization <= this.config.REORG_MAX_EPOCHS_SINCE_FINALIZATION;
|
|
1568
|
+
if (!isFinalizationOk) {
|
|
1569
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.ChainLongUnfinality};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// No reorg if this reorg spans more than a single slot
|
|
1573
|
+
const parentSlotOk = parentBlock.slot + 1 === headBlock.slot;
|
|
1574
|
+
if (!parentSlotOk) {
|
|
1575
|
+
return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.ParentBlockDistanceMoreThanOneSlot};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
prelimProposerHead = parentBlock;
|
|
1579
|
+
|
|
1580
|
+
return {prelimProposerHead};
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* This function checks the terminal pow conditions on the merge block as
|
|
1586
|
+
* specified in the config either via TTD or TBH. This function is part of
|
|
1587
|
+
* forkChoice because if the merge block was previously imported as syncing
|
|
1588
|
+
* and the EL eventually signals it catching up via validateLatestHash
|
|
1589
|
+
* the specs mandates validating terminal conditions on the previously
|
|
1590
|
+
* imported merge block.
|
|
1591
|
+
*/
|
|
1592
|
+
export function assertValidTerminalPowBlock(
|
|
1593
|
+
config: ChainConfig,
|
|
1594
|
+
block: bellatrix.BeaconBlock,
|
|
1595
|
+
preCachedData: {
|
|
1596
|
+
executionStatus: ExecutionStatus.Syncing | ExecutionStatus.Valid;
|
|
1597
|
+
powBlock?: PowBlockHex | null;
|
|
1598
|
+
powBlockParent?: PowBlockHex | null;
|
|
1599
|
+
}
|
|
1600
|
+
): void {
|
|
1601
|
+
if (!ssz.Root.equals(config.TERMINAL_BLOCK_HASH, ZERO_HASH)) {
|
|
1602
|
+
if (computeEpochAtSlot(block.slot) < config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH)
|
|
1603
|
+
throw Error(`Terminal block activation epoch ${config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH} not reached`);
|
|
1604
|
+
|
|
1605
|
+
// powBock.blockHash is hex, so we just pick the corresponding root
|
|
1606
|
+
if (!ssz.Root.equals(block.body.executionPayload.parentHash, config.TERMINAL_BLOCK_HASH))
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
`Invalid terminal block hash, expected: ${toRootHex(config.TERMINAL_BLOCK_HASH)}, actual: ${toRootHex(
|
|
1609
|
+
block.body.executionPayload.parentHash
|
|
1610
|
+
)}`
|
|
1611
|
+
);
|
|
1612
|
+
} else {
|
|
1613
|
+
// If no TERMINAL_BLOCK_HASH override, check ttd
|
|
1614
|
+
|
|
1615
|
+
// Delay powBlock checks if the payload execution status is unknown because of
|
|
1616
|
+
// syncing response in notifyNewPayload call while verifying
|
|
1617
|
+
if (preCachedData?.executionStatus === ExecutionStatus.Syncing) return;
|
|
1618
|
+
|
|
1619
|
+
const {powBlock, powBlockParent} = preCachedData;
|
|
1620
|
+
if (!powBlock) throw Error("onBlock preCachedData must include powBlock");
|
|
1621
|
+
// if powBlock is genesis don't assert powBlockParent
|
|
1622
|
+
if (!powBlockParent && powBlock.parentHash !== HEX_ZERO_HASH)
|
|
1623
|
+
throw Error("onBlock preCachedData must include powBlockParent");
|
|
1624
|
+
|
|
1625
|
+
const isTotalDifficultyReached = powBlock.totalDifficulty >= config.TERMINAL_TOTAL_DIFFICULTY;
|
|
1626
|
+
// If we don't have powBlockParent here, powBlock is the genesis and as we would have errored above
|
|
1627
|
+
// we can mark isParentTotalDifficultyValid as valid
|
|
1628
|
+
const isParentTotalDifficultyValid =
|
|
1629
|
+
!powBlockParent || powBlockParent.totalDifficulty < config.TERMINAL_TOTAL_DIFFICULTY;
|
|
1630
|
+
if (!isTotalDifficultyReached) {
|
|
1631
|
+
throw Error(
|
|
1632
|
+
`Invalid terminal POW block: total difficulty not reached expected >= ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlock.totalDifficulty}`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (!isParentTotalDifficultyValid) {
|
|
1637
|
+
throw Error(
|
|
1638
|
+
`Invalid terminal POW block parent: expected < ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlockParent.totalDifficulty}`
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// Approximate https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#calculate_committee_fraction
|
|
1644
|
+
// Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
|
|
1645
|
+
export function getCommitteeFraction(
|
|
1646
|
+
justifiedTotalActiveBalanceByIncrement: number,
|
|
1647
|
+
config: {slotsPerEpoch: number; committeePercent: number}
|
|
1648
|
+
): number {
|
|
1649
|
+
const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch);
|
|
1650
|
+
return Math.floor((committeeWeight * config.committeePercent) / 100);
|
|
1651
|
+
}
|