@lodestar/fork-choice 1.35.0-dev.b42a298a7c → 1.35.0-dev.ba92bd8a88
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/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 +10 -12
- package/src/forkChoice/errors.ts +98 -0
- package/src/forkChoice/forkChoice.ts +1648 -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,1076 @@
|
|
|
1
|
+
import {GENESIS_EPOCH} from "@lodestar/params";
|
|
2
|
+
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
3
|
+
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
4
|
+
import {toRootHex} from "@lodestar/utils";
|
|
5
|
+
import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
|
|
6
|
+
import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
|
|
7
|
+
import {ExecutionStatus, HEX_ZERO_HASH, LVHExecResponse, ProtoBlock, ProtoNode} from "./interface.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
10
|
+
type ProposerBoost = {root: RootHex; score: number};
|
|
11
|
+
|
|
12
|
+
const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
|
|
13
|
+
|
|
14
|
+
export class ProtoArray {
|
|
15
|
+
// Do not attempt to prune the tree unless it has at least this many nodes.
|
|
16
|
+
// Small prunes simply waste time
|
|
17
|
+
pruneThreshold: number;
|
|
18
|
+
justifiedEpoch: Epoch;
|
|
19
|
+
justifiedRoot: RootHex;
|
|
20
|
+
finalizedEpoch: Epoch;
|
|
21
|
+
finalizedRoot: RootHex;
|
|
22
|
+
nodes: ProtoNode[] = [];
|
|
23
|
+
indices = new Map<RootHex, number>();
|
|
24
|
+
lvhError?: LVHExecError;
|
|
25
|
+
|
|
26
|
+
private previousProposerBoost: ProposerBoost | null = null;
|
|
27
|
+
|
|
28
|
+
constructor({
|
|
29
|
+
pruneThreshold,
|
|
30
|
+
justifiedEpoch,
|
|
31
|
+
justifiedRoot,
|
|
32
|
+
finalizedEpoch,
|
|
33
|
+
finalizedRoot,
|
|
34
|
+
}: {
|
|
35
|
+
pruneThreshold: number;
|
|
36
|
+
justifiedEpoch: Epoch;
|
|
37
|
+
justifiedRoot: RootHex;
|
|
38
|
+
finalizedEpoch: Epoch;
|
|
39
|
+
finalizedRoot: RootHex;
|
|
40
|
+
}) {
|
|
41
|
+
this.pruneThreshold = pruneThreshold;
|
|
42
|
+
this.justifiedEpoch = justifiedEpoch;
|
|
43
|
+
this.justifiedRoot = justifiedRoot;
|
|
44
|
+
this.finalizedEpoch = finalizedEpoch;
|
|
45
|
+
this.finalizedRoot = finalizedRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static initialize(block: Omit<ProtoBlock, "targetRoot">, currentSlot: Slot): ProtoArray {
|
|
49
|
+
const protoArray = new ProtoArray({
|
|
50
|
+
pruneThreshold: DEFAULT_PRUNE_THRESHOLD,
|
|
51
|
+
justifiedEpoch: block.justifiedEpoch,
|
|
52
|
+
justifiedRoot: block.justifiedRoot,
|
|
53
|
+
finalizedEpoch: block.finalizedEpoch,
|
|
54
|
+
finalizedRoot: block.finalizedRoot,
|
|
55
|
+
});
|
|
56
|
+
protoArray.onBlock(
|
|
57
|
+
{
|
|
58
|
+
...block,
|
|
59
|
+
// We are using the blockROot as the targetRoot, since it always lies on an epoch boundary
|
|
60
|
+
targetRoot: block.blockRoot,
|
|
61
|
+
} as ProtoBlock,
|
|
62
|
+
currentSlot
|
|
63
|
+
);
|
|
64
|
+
return protoArray;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Iterate backwards through the array, touching all nodes and their parents and potentially
|
|
69
|
+
* the best-child of each parent.
|
|
70
|
+
*
|
|
71
|
+
* The structure of the `self.nodes` array ensures that the child of each node is always
|
|
72
|
+
* touched before its parent.
|
|
73
|
+
*
|
|
74
|
+
* For each node, the following is done:
|
|
75
|
+
*
|
|
76
|
+
* - Update the node's weight with the corresponding delta.
|
|
77
|
+
* - Back-propagate each node's delta to its parents delta.
|
|
78
|
+
* - Compare the current node with the parents best-child, updating it if the current node
|
|
79
|
+
* should become the best child.
|
|
80
|
+
* - If required, update the parents best-descendant with the current node or its best-descendant.
|
|
81
|
+
*/
|
|
82
|
+
applyScoreChanges({
|
|
83
|
+
deltas,
|
|
84
|
+
proposerBoost,
|
|
85
|
+
justifiedEpoch,
|
|
86
|
+
justifiedRoot,
|
|
87
|
+
finalizedEpoch,
|
|
88
|
+
finalizedRoot,
|
|
89
|
+
currentSlot,
|
|
90
|
+
}: {
|
|
91
|
+
deltas: number[];
|
|
92
|
+
proposerBoost: ProposerBoost | null;
|
|
93
|
+
justifiedEpoch: Epoch;
|
|
94
|
+
justifiedRoot: RootHex;
|
|
95
|
+
finalizedEpoch: Epoch;
|
|
96
|
+
finalizedRoot: RootHex;
|
|
97
|
+
currentSlot: Slot;
|
|
98
|
+
}): void {
|
|
99
|
+
if (deltas.length !== this.indices.size) {
|
|
100
|
+
throw new ProtoArrayError({
|
|
101
|
+
code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
|
|
102
|
+
deltas: deltas.length,
|
|
103
|
+
indices: this.indices.size,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
justifiedEpoch !== this.justifiedEpoch ||
|
|
109
|
+
finalizedEpoch !== this.finalizedEpoch ||
|
|
110
|
+
justifiedRoot !== this.justifiedRoot ||
|
|
111
|
+
finalizedRoot !== this.finalizedRoot
|
|
112
|
+
) {
|
|
113
|
+
this.justifiedEpoch = justifiedEpoch;
|
|
114
|
+
this.finalizedEpoch = finalizedEpoch;
|
|
115
|
+
this.justifiedRoot = justifiedRoot;
|
|
116
|
+
this.finalizedRoot = finalizedRoot;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Iterate backwards through all indices in this.nodes
|
|
120
|
+
for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
|
|
121
|
+
const node = this.nodes[nodeIndex];
|
|
122
|
+
if (node === undefined) {
|
|
123
|
+
throw new ProtoArrayError({
|
|
124
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
125
|
+
index: nodeIndex,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// There is no need to adjust the balances or manage parent of the zero hash since it
|
|
130
|
+
// is an alias to the genesis block. The weight applied to the genesis block is
|
|
131
|
+
// irrelevant as we _always_ choose it and it's impossible for it to have a parent.
|
|
132
|
+
if (node.blockRoot === HEX_ZERO_HASH) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const currentBoost = proposerBoost && proposerBoost.root === node.blockRoot ? proposerBoost.score : 0;
|
|
137
|
+
const previousBoost =
|
|
138
|
+
this.previousProposerBoost && this.previousProposerBoost.root === node.blockRoot
|
|
139
|
+
? this.previousProposerBoost.score
|
|
140
|
+
: 0;
|
|
141
|
+
|
|
142
|
+
// If this node's execution status has been marked invalid, then the weight of the node
|
|
143
|
+
// needs to be taken out of consideration after which the node weight will become 0
|
|
144
|
+
// for subsequent iterations of applyScoreChanges
|
|
145
|
+
const nodeDelta =
|
|
146
|
+
node.executionStatus === ExecutionStatus.Invalid
|
|
147
|
+
? -node.weight
|
|
148
|
+
: deltas[nodeIndex] + currentBoost - previousBoost;
|
|
149
|
+
|
|
150
|
+
// Apply the delta to the node
|
|
151
|
+
node.weight += nodeDelta;
|
|
152
|
+
|
|
153
|
+
// Update the parent delta (if any)
|
|
154
|
+
const parentIndex = node.parent;
|
|
155
|
+
if (parentIndex !== undefined) {
|
|
156
|
+
const parentDelta = deltas[parentIndex];
|
|
157
|
+
if (parentDelta === undefined) {
|
|
158
|
+
throw new ProtoArrayError({
|
|
159
|
+
code: ProtoArrayErrorCode.INVALID_PARENT_DELTA,
|
|
160
|
+
index: parentIndex,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// back-propagate the nodes delta to its parent
|
|
165
|
+
deltas[parentIndex] += nodeDelta;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A second time, iterate backwards through all indices in `this.nodes`.
|
|
170
|
+
//
|
|
171
|
+
// We _must_ perform these functions separate from the weight-updating loop above to ensure
|
|
172
|
+
// that we have a fully coherent set of weights before updating parent
|
|
173
|
+
// best-child/descendant.
|
|
174
|
+
for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
|
|
175
|
+
const node = this.nodes[nodeIndex];
|
|
176
|
+
if (node === undefined) {
|
|
177
|
+
throw new ProtoArrayError({
|
|
178
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
179
|
+
index: nodeIndex,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// If the node has a parent, try to update its best-child and best-descendant.
|
|
184
|
+
const parentIndex = node.parent;
|
|
185
|
+
if (parentIndex !== undefined) {
|
|
186
|
+
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Update the previous proposer boost
|
|
190
|
+
this.previousProposerBoost = proposerBoost;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register a block with the fork choice.
|
|
195
|
+
*
|
|
196
|
+
* It is only sane to supply an undefined parent for the genesis block
|
|
197
|
+
*/
|
|
198
|
+
onBlock(block: ProtoBlock, currentSlot: Slot): void {
|
|
199
|
+
// If the block is already known, simply ignore it
|
|
200
|
+
if (this.indices.has(block.blockRoot)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (block.executionStatus === ExecutionStatus.Invalid) {
|
|
204
|
+
throw new ProtoArrayError({
|
|
205
|
+
code: ProtoArrayErrorCode.INVALID_BLOCK_EXECUTION_STATUS,
|
|
206
|
+
root: block.blockRoot,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const node: ProtoNode = {
|
|
211
|
+
...block,
|
|
212
|
+
parent: this.indices.get(block.parentRoot),
|
|
213
|
+
weight: 0,
|
|
214
|
+
bestChild: undefined,
|
|
215
|
+
bestDescendant: undefined,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const nodeIndex = this.nodes.length;
|
|
219
|
+
|
|
220
|
+
this.indices.set(node.blockRoot, nodeIndex);
|
|
221
|
+
this.nodes.push(node);
|
|
222
|
+
|
|
223
|
+
// If this node is valid, lets propagate the valid status up the chain
|
|
224
|
+
// and throw error if we counter invalid, as this breaks consensus
|
|
225
|
+
if (node.parent !== undefined) {
|
|
226
|
+
this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot);
|
|
227
|
+
|
|
228
|
+
if (node.executionStatus === ExecutionStatus.Valid) {
|
|
229
|
+
this.propagateValidExecutionStatusByIndex(node.parent);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Optimistic sync validate till validated latest hash, invalidate any descendant branch
|
|
236
|
+
* if invalidate till hash provided. If consensus fails, this will invalidate entire
|
|
237
|
+
* forkChoice which will throw on any call to findHead
|
|
238
|
+
*/
|
|
239
|
+
validateLatestHash(execResponse: LVHExecResponse, currentSlot: Slot): void {
|
|
240
|
+
// Look reverse because its highly likely node with latestValidExecHash is towards the
|
|
241
|
+
// the leaves of the forkchoice
|
|
242
|
+
//
|
|
243
|
+
// We can also implement the index to lookup for exec hash => proto block, but it
|
|
244
|
+
// still needs to be established properly (though is highly likely) than a unique
|
|
245
|
+
// exec hash maps to a unique beacon block.
|
|
246
|
+
// For more context on this please checkout the following conversation:
|
|
247
|
+
// https://github.com/ChainSafe/lodestar/pull/4182#discussion_r914770167
|
|
248
|
+
|
|
249
|
+
if (execResponse.executionStatus === ExecutionStatus.Valid) {
|
|
250
|
+
const {latestValidExecHash} = execResponse;
|
|
251
|
+
// We use -1 for denoting not found
|
|
252
|
+
let latestValidHashIndex = -1;
|
|
253
|
+
|
|
254
|
+
for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
|
|
255
|
+
if (this.nodes[nodeIndex].executionPayloadBlockHash === latestValidExecHash) {
|
|
256
|
+
latestValidHashIndex = nodeIndex;
|
|
257
|
+
// We found the block corresponding to latestValidHashIndex, exit the loop
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// We are trying to be as forgiving as possible here because ideally latestValidHashIndex
|
|
263
|
+
// should be found in the forkchoice
|
|
264
|
+
if (latestValidHashIndex >= 0) {
|
|
265
|
+
this.propagateValidExecutionStatusByIndex(latestValidHashIndex);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// In case of invalidation, ideally:
|
|
269
|
+
// i) Find the invalid payload
|
|
270
|
+
// ii) Obtain a chain [LVH.child, LVH.child.child, ....., invalid_payload]
|
|
271
|
+
// iii) Obtain a chain [Last_known_valid_node, ...., LVH]
|
|
272
|
+
//
|
|
273
|
+
// Mark chain iii) as Valid if LVH is non null but right now LVH can be non null without
|
|
274
|
+
// gurranteing chain iii) to be valid: for e.g. in following scenario LVH can be returned
|
|
275
|
+
// as any of SYNCING: SYNCING, SYNCING, SYNCING, INVALID (due to simple check)/
|
|
276
|
+
// So we currently ignore this chain and hope eventually it gets resolved
|
|
277
|
+
//
|
|
278
|
+
// Mark chain ii) as Invalid if LVH is found and non null, else only invalidate invalid_payload
|
|
279
|
+
// if its in fcU.
|
|
280
|
+
//
|
|
281
|
+
const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
|
|
282
|
+
const invalidateFromParentIndex = this.indices.get(invalidateFromParentBlockRoot);
|
|
283
|
+
if (invalidateFromParentIndex === undefined) {
|
|
284
|
+
throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
|
|
285
|
+
}
|
|
286
|
+
const latestValidHashIndex =
|
|
287
|
+
latestValidExecHash !== null ? this.getNodeIndexFromLVH(latestValidExecHash, invalidateFromParentIndex) : null;
|
|
288
|
+
if (latestValidHashIndex === null) {
|
|
289
|
+
/**
|
|
290
|
+
* The LVH (latest valid hash) is null or not found.
|
|
291
|
+
*
|
|
292
|
+
* The spec gives an allowance for the EL being able to return a nullish LVH if it could not
|
|
293
|
+
* "determine" one. There are two interpretations:
|
|
294
|
+
*
|
|
295
|
+
* - "the LVH is unknown" - simply throw and move on. We can't determine which chain to invalidate
|
|
296
|
+
* since we don't know which ancestor is valid.
|
|
297
|
+
*
|
|
298
|
+
* - "the LVH doesn't exist" - this means that the entire ancestor chain is invalid, and should
|
|
299
|
+
* be marked as such.
|
|
300
|
+
*
|
|
301
|
+
* The more robust approach is to treat nullish LVH as "the LVH is unknown" rather than
|
|
302
|
+
* "the LVH doesn't exist". The alternative means that we will poison a valid chain when the
|
|
303
|
+
* EL is lazy (or buggy) with its LVH response.
|
|
304
|
+
*/
|
|
305
|
+
throw Error(`Unable to find latestValidExecHash=${latestValidExecHash} in the forkchoice`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.propagateInValidExecutionStatusByIndex(invalidateFromParentIndex, latestValidHashIndex, currentSlot);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private propagateValidExecutionStatusByIndex(validNodeIndex: number): void {
|
|
313
|
+
let nodeIndex: number | undefined = validNodeIndex;
|
|
314
|
+
// propagate till we keep encountering syncing status
|
|
315
|
+
while (nodeIndex !== undefined) {
|
|
316
|
+
const node = this.getNodeFromIndex(nodeIndex);
|
|
317
|
+
if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
this.validateNodeByIndex(nodeIndex);
|
|
321
|
+
nodeIndex = node.parent;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Do a two pass invalidation:
|
|
327
|
+
* 1. we go up and mark all nodes invalid and then
|
|
328
|
+
* 2. we need do iterate down and mark all children of invalid nodes invalid
|
|
329
|
+
*
|
|
330
|
+
* latestValidHashIndex === undefined implies invalidate only invalidateTillIndex
|
|
331
|
+
* latestValidHashIndex === -1 implies invalidate all post merge blocks
|
|
332
|
+
* latestValidHashIndex >=0 implies invalidate the chain upwards from invalidateTillIndex
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
private propagateInValidExecutionStatusByIndex(
|
|
336
|
+
invalidateFromParentIndex: number,
|
|
337
|
+
latestValidHashIndex: number,
|
|
338
|
+
currentSlot: Slot
|
|
339
|
+
): void {
|
|
340
|
+
// Pass 1: mark invalidateFromParentIndex and its parents invalid
|
|
341
|
+
let invalidateIndex: number | undefined = invalidateFromParentIndex;
|
|
342
|
+
while (invalidateIndex !== undefined && invalidateIndex > latestValidHashIndex) {
|
|
343
|
+
const invalidNode = this.invalidateNodeByIndex(invalidateIndex);
|
|
344
|
+
invalidateIndex = invalidNode.parent;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Pass 2: mark all children of invalid nodes as invalid
|
|
348
|
+
for (let nodeIndex = 0; nodeIndex < this.nodes.length; nodeIndex++) {
|
|
349
|
+
const node = this.getNodeFromIndex(nodeIndex);
|
|
350
|
+
const parent = node.parent !== undefined ? this.getNodeByIndex(node.parent) : undefined;
|
|
351
|
+
// Only invalidate if this is post merge, and either parent is invalid or the
|
|
352
|
+
// consensus has failed
|
|
353
|
+
if (parent?.executionStatus === ExecutionStatus.Invalid) {
|
|
354
|
+
// check and flip node status to invalid
|
|
355
|
+
this.invalidateNodeByIndex(nodeIndex);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// update the forkchoice as the invalidation can change the entire forkchoice DAG
|
|
360
|
+
this.applyScoreChanges({
|
|
361
|
+
deltas: Array.from({length: this.nodes.length}, () => 0),
|
|
362
|
+
proposerBoost: this.previousProposerBoost,
|
|
363
|
+
justifiedEpoch: this.justifiedEpoch,
|
|
364
|
+
justifiedRoot: this.justifiedRoot,
|
|
365
|
+
finalizedEpoch: this.finalizedEpoch,
|
|
366
|
+
finalizedRoot: this.finalizedRoot,
|
|
367
|
+
currentSlot,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private getNodeIndexFromLVH(latestValidExecHash: RootHex, ancestorFromIndex: number): number | null {
|
|
372
|
+
let nodeIndex: number | undefined = ancestorFromIndex;
|
|
373
|
+
while (nodeIndex !== undefined && nodeIndex >= 0) {
|
|
374
|
+
const node = this.getNodeFromIndex(nodeIndex);
|
|
375
|
+
if (
|
|
376
|
+
(node.executionStatus === ExecutionStatus.PreMerge && latestValidExecHash === ZERO_HASH_HEX) ||
|
|
377
|
+
node.executionPayloadBlockHash === latestValidExecHash
|
|
378
|
+
) {
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
nodeIndex = node.parent;
|
|
382
|
+
}
|
|
383
|
+
return nodeIndex !== undefined ? nodeIndex : null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private invalidateNodeByIndex(nodeIndex: number): ProtoNode {
|
|
387
|
+
const invalidNode = this.getNodeFromIndex(nodeIndex);
|
|
388
|
+
|
|
389
|
+
// If node to be invalidated is pre-merge or valid,it is a catastrophe,
|
|
390
|
+
// and indicates consensus failure and a non recoverable damage.
|
|
391
|
+
//
|
|
392
|
+
// There is no further processing that can be done.
|
|
393
|
+
// Just assign error for marking proto-array perma damaged and throw!
|
|
394
|
+
if (
|
|
395
|
+
invalidNode.executionStatus === ExecutionStatus.Valid ||
|
|
396
|
+
invalidNode.executionStatus === ExecutionStatus.PreMerge
|
|
397
|
+
) {
|
|
398
|
+
const lvhCode =
|
|
399
|
+
invalidNode.executionStatus === ExecutionStatus.Valid
|
|
400
|
+
? LVHExecErrorCode.ValidToInvalid
|
|
401
|
+
: LVHExecErrorCode.PreMergeToInvalid;
|
|
402
|
+
|
|
403
|
+
this.lvhError = {
|
|
404
|
+
lvhCode,
|
|
405
|
+
blockRoot: invalidNode.blockRoot,
|
|
406
|
+
execHash: invalidNode.executionPayloadBlockHash ?? ZERO_HASH_HEX,
|
|
407
|
+
};
|
|
408
|
+
throw new ProtoArrayError({
|
|
409
|
+
code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
|
|
410
|
+
...this.lvhError,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
invalidNode.executionStatus = ExecutionStatus.Invalid;
|
|
415
|
+
invalidNode.bestChild = undefined;
|
|
416
|
+
invalidNode.bestDescendant = undefined;
|
|
417
|
+
|
|
418
|
+
return invalidNode;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private validateNodeByIndex(nodeIndex: number): ProtoNode {
|
|
422
|
+
const validNode = this.getNodeFromIndex(nodeIndex);
|
|
423
|
+
if (validNode.executionStatus === ExecutionStatus.Invalid) {
|
|
424
|
+
this.lvhError = {
|
|
425
|
+
lvhCode: LVHExecErrorCode.InvalidToValid,
|
|
426
|
+
blockRoot: validNode.blockRoot,
|
|
427
|
+
execHash: validNode.executionPayloadBlockHash,
|
|
428
|
+
};
|
|
429
|
+
throw new ProtoArrayError({
|
|
430
|
+
code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
|
|
431
|
+
...this.lvhError,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (validNode.executionStatus === ExecutionStatus.Syncing) {
|
|
436
|
+
validNode.executionStatus = ExecutionStatus.Valid;
|
|
437
|
+
}
|
|
438
|
+
return validNode;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Follows the best-descendant links to find the best-block (i.e., head-block).
|
|
443
|
+
*/
|
|
444
|
+
findHead(justifiedRoot: RootHex, currentSlot: Slot): RootHex {
|
|
445
|
+
if (this.lvhError) {
|
|
446
|
+
throw new ProtoArrayError({
|
|
447
|
+
code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
|
|
448
|
+
...this.lvhError,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const justifiedIndex = this.indices.get(justifiedRoot);
|
|
453
|
+
if (justifiedIndex === undefined) {
|
|
454
|
+
throw new ProtoArrayError({
|
|
455
|
+
code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN,
|
|
456
|
+
root: justifiedRoot,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const justifiedNode = this.nodes[justifiedIndex];
|
|
461
|
+
if (justifiedNode === undefined) {
|
|
462
|
+
throw new ProtoArrayError({
|
|
463
|
+
code: ProtoArrayErrorCode.INVALID_JUSTIFIED_INDEX,
|
|
464
|
+
index: justifiedIndex,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (justifiedNode.executionStatus === ExecutionStatus.Invalid) {
|
|
469
|
+
throw new ProtoArrayError({
|
|
470
|
+
code: ProtoArrayErrorCode.INVALID_JUSTIFIED_EXECUTION_STATUS,
|
|
471
|
+
root: justifiedNode.blockRoot,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const bestDescendantIndex = justifiedNode.bestDescendant ?? justifiedIndex;
|
|
476
|
+
|
|
477
|
+
const bestNode = this.nodes[bestDescendantIndex];
|
|
478
|
+
if (bestNode === undefined) {
|
|
479
|
+
throw new ProtoArrayError({
|
|
480
|
+
code: ProtoArrayErrorCode.INVALID_BEST_DESCENDANT_INDEX,
|
|
481
|
+
index: bestDescendantIndex,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Perform a sanity check that the node is indeed valid to be the head
|
|
487
|
+
* The justified node is always considered viable for head per spec:
|
|
488
|
+
* def get_head(store: Store) -> Root:
|
|
489
|
+
* blocks = get_filtered_block_tree(store)
|
|
490
|
+
* head = store.justified_checkpoint.root
|
|
491
|
+
*/
|
|
492
|
+
if (bestDescendantIndex !== justifiedIndex && !this.nodeIsViableForHead(bestNode, currentSlot)) {
|
|
493
|
+
throw new ProtoArrayError({
|
|
494
|
+
code: ProtoArrayErrorCode.INVALID_BEST_NODE,
|
|
495
|
+
startRoot: justifiedRoot,
|
|
496
|
+
justifiedEpoch: this.justifiedEpoch,
|
|
497
|
+
finalizedEpoch: this.finalizedEpoch,
|
|
498
|
+
headRoot: justifiedNode.blockRoot,
|
|
499
|
+
headJustifiedEpoch: justifiedNode.justifiedEpoch,
|
|
500
|
+
headFinalizedEpoch: justifiedNode.finalizedEpoch,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return bestNode.blockRoot;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Update the tree with new finalization information. The tree is only actually pruned if both
|
|
509
|
+
* of the two following criteria are met:
|
|
510
|
+
*
|
|
511
|
+
* - The supplied finalized epoch and root are different to the current values.
|
|
512
|
+
* - The number of nodes in `self` is at least `self.prune_threshold`.
|
|
513
|
+
*
|
|
514
|
+
* # Errors
|
|
515
|
+
*
|
|
516
|
+
* Returns errors if:
|
|
517
|
+
*
|
|
518
|
+
* - The finalized epoch is less than the current one.
|
|
519
|
+
* - The finalized epoch is equal to the current one, but the finalized root is different.
|
|
520
|
+
* - There is some internal error relating to invalid indices inside `this`.
|
|
521
|
+
*/
|
|
522
|
+
maybePrune(finalizedRoot: RootHex): ProtoBlock[] {
|
|
523
|
+
const finalizedIndex = this.indices.get(finalizedRoot);
|
|
524
|
+
if (finalizedIndex === undefined) {
|
|
525
|
+
throw new ProtoArrayError({
|
|
526
|
+
code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN,
|
|
527
|
+
root: finalizedRoot,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (finalizedIndex < this.pruneThreshold) {
|
|
532
|
+
// Pruning at small numbers incurs more cost than benefit
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Remove the this.indices key/values for all the to-be-deleted nodes
|
|
537
|
+
for (let nodeIndex = 0; nodeIndex < finalizedIndex; nodeIndex++) {
|
|
538
|
+
const node = this.nodes[nodeIndex];
|
|
539
|
+
if (node === undefined) {
|
|
540
|
+
throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index: nodeIndex});
|
|
541
|
+
}
|
|
542
|
+
this.indices.delete(node.blockRoot);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Store nodes prior to finalization
|
|
546
|
+
const removed = this.nodes.slice(0, finalizedIndex);
|
|
547
|
+
// Drop all the nodes prior to finalization
|
|
548
|
+
this.nodes = this.nodes.slice(finalizedIndex);
|
|
549
|
+
|
|
550
|
+
// Adjust the indices map
|
|
551
|
+
for (const [key, value] of this.indices.entries()) {
|
|
552
|
+
if (value < finalizedIndex) {
|
|
553
|
+
throw new ProtoArrayError({
|
|
554
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
555
|
+
value: "indices",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
this.indices.set(key, value - finalizedIndex);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Iterate through all the existing nodes and adjust their indices to match the new layout of this.nodes
|
|
562
|
+
for (let i = 0, len = this.nodes.length; i < len; i++) {
|
|
563
|
+
const node = this.nodes[i];
|
|
564
|
+
const parentIndex = node.parent;
|
|
565
|
+
if (parentIndex !== undefined) {
|
|
566
|
+
// If node.parent is less than finalizedIndex, set it to undefined
|
|
567
|
+
node.parent = parentIndex < finalizedIndex ? undefined : parentIndex - finalizedIndex;
|
|
568
|
+
}
|
|
569
|
+
const bestChild = node.bestChild;
|
|
570
|
+
if (bestChild !== undefined) {
|
|
571
|
+
if (bestChild < finalizedIndex) {
|
|
572
|
+
throw new ProtoArrayError({
|
|
573
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
574
|
+
value: "bestChild",
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
node.bestChild = bestChild - finalizedIndex;
|
|
578
|
+
}
|
|
579
|
+
const bestDescendant = node.bestDescendant;
|
|
580
|
+
if (bestDescendant !== undefined) {
|
|
581
|
+
if (bestDescendant < finalizedIndex) {
|
|
582
|
+
throw new ProtoArrayError({
|
|
583
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
584
|
+
value: "bestDescendant",
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
node.bestDescendant = bestDescendant - finalizedIndex;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return removed;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Observe the parent at `parent_index` with respect to the child at `child_index` and
|
|
595
|
+
* potentially modify the `parent.best_child` and `parent.best_descendant` values.
|
|
596
|
+
*
|
|
597
|
+
* ## Detail
|
|
598
|
+
*
|
|
599
|
+
* There are four outcomes:
|
|
600
|
+
*
|
|
601
|
+
* - The child is already the best child but it's now invalid due to a FFG change and should be removed.
|
|
602
|
+
* - The child is already the best child and the parent is updated with the new
|
|
603
|
+
* best-descendant.
|
|
604
|
+
* - The child is not the best child but becomes the best child.
|
|
605
|
+
* - The child is not the best child and does not become the best child.
|
|
606
|
+
*/
|
|
607
|
+
maybeUpdateBestChildAndDescendant(parentIndex: number, childIndex: number, currentSlot: Slot): void {
|
|
608
|
+
const childNode = this.nodes[childIndex];
|
|
609
|
+
if (childNode === undefined) {
|
|
610
|
+
throw new ProtoArrayError({
|
|
611
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
612
|
+
index: childIndex,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const parentNode = this.nodes[parentIndex];
|
|
617
|
+
if (parentNode === undefined) {
|
|
618
|
+
throw new ProtoArrayError({
|
|
619
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
620
|
+
index: parentIndex,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const childLeadsToViableHead = this.nodeLeadsToViableHead(childNode, currentSlot);
|
|
625
|
+
|
|
626
|
+
// These three variables are aliases to the three options that we may set the
|
|
627
|
+
// parent.bestChild and parent.bestDescendent to.
|
|
628
|
+
//
|
|
629
|
+
// Aliases are used to assist readability.
|
|
630
|
+
type ChildAndDescendant = [number | undefined, number | undefined];
|
|
631
|
+
const changeToNull: ChildAndDescendant = [undefined, undefined];
|
|
632
|
+
const changeToChild: ChildAndDescendant = [childIndex, childNode.bestDescendant ?? childIndex];
|
|
633
|
+
const noChange: ChildAndDescendant = [parentNode.bestChild, parentNode.bestDescendant];
|
|
634
|
+
|
|
635
|
+
let newChildAndDescendant: ChildAndDescendant;
|
|
636
|
+
const bestChildIndex = parentNode.bestChild;
|
|
637
|
+
if (bestChildIndex !== undefined) {
|
|
638
|
+
if (bestChildIndex === childIndex && !childLeadsToViableHead) {
|
|
639
|
+
// the child is already the best-child of the parent but its not viable for the head
|
|
640
|
+
// so remove it
|
|
641
|
+
newChildAndDescendant = changeToNull;
|
|
642
|
+
} else if (bestChildIndex === childIndex) {
|
|
643
|
+
// the child is the best-child already
|
|
644
|
+
// set it again to ensure that the best-descendent of the parent is updated
|
|
645
|
+
newChildAndDescendant = changeToChild;
|
|
646
|
+
} else {
|
|
647
|
+
const bestChildNode = this.nodes[bestChildIndex];
|
|
648
|
+
if (bestChildNode === undefined) {
|
|
649
|
+
throw new ProtoArrayError({
|
|
650
|
+
code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX,
|
|
651
|
+
index: bestChildIndex,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const bestChildLeadsToViableHead = this.nodeLeadsToViableHead(bestChildNode, currentSlot);
|
|
656
|
+
|
|
657
|
+
if (childLeadsToViableHead && !bestChildLeadsToViableHead) {
|
|
658
|
+
// the child leads to a viable head, but the current best-child doesn't
|
|
659
|
+
newChildAndDescendant = changeToChild;
|
|
660
|
+
} else if (!childLeadsToViableHead && bestChildLeadsToViableHead) {
|
|
661
|
+
// the best child leads to a viable head but the child doesn't
|
|
662
|
+
newChildAndDescendant = noChange;
|
|
663
|
+
} else if (childNode.weight === bestChildNode.weight) {
|
|
664
|
+
// tie-breaker of equal weights by root
|
|
665
|
+
if (childNode.blockRoot >= bestChildNode.blockRoot) {
|
|
666
|
+
newChildAndDescendant = changeToChild;
|
|
667
|
+
} else {
|
|
668
|
+
newChildAndDescendant = noChange;
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
// choose the winner by weight
|
|
672
|
+
if (childNode.weight >= bestChildNode.weight) {
|
|
673
|
+
newChildAndDescendant = changeToChild;
|
|
674
|
+
} else {
|
|
675
|
+
newChildAndDescendant = noChange;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} else if (childLeadsToViableHead) {
|
|
680
|
+
// There is no current best-child and the child is viable.
|
|
681
|
+
newChildAndDescendant = changeToChild;
|
|
682
|
+
} else {
|
|
683
|
+
// There is no current best-child but the child is not viable.
|
|
684
|
+
newChildAndDescendant = noChange;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
parentNode.bestChild = newChildAndDescendant[0];
|
|
688
|
+
parentNode.bestDescendant = newChildAndDescendant[1];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Indicates if the node itself is viable for the head, or if it's best descendant is viable
|
|
693
|
+
* for the head.
|
|
694
|
+
*/
|
|
695
|
+
nodeLeadsToViableHead(node: ProtoNode, currentSlot: Slot): boolean {
|
|
696
|
+
let bestDescendantIsViableForHead: boolean;
|
|
697
|
+
const bestDescendantIndex = node.bestDescendant;
|
|
698
|
+
if (bestDescendantIndex !== undefined) {
|
|
699
|
+
const bestDescendantNode = this.nodes[bestDescendantIndex];
|
|
700
|
+
if (bestDescendantNode === undefined) {
|
|
701
|
+
throw new ProtoArrayError({
|
|
702
|
+
code: ProtoArrayErrorCode.INVALID_BEST_DESCENDANT_INDEX,
|
|
703
|
+
index: bestDescendantIndex,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
bestDescendantIsViableForHead = this.nodeIsViableForHead(bestDescendantNode, currentSlot);
|
|
707
|
+
} else {
|
|
708
|
+
bestDescendantIsViableForHead = false;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return bestDescendantIsViableForHead || this.nodeIsViableForHead(node, currentSlot);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* This is the equivalent to the `filter_block_tree` function in the Ethereum Consensus spec:
|
|
716
|
+
*
|
|
717
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#filter_block_tree
|
|
718
|
+
*
|
|
719
|
+
* Any node that has a different finalized or justified epoch should not be viable for the
|
|
720
|
+
* head.
|
|
721
|
+
*/
|
|
722
|
+
nodeIsViableForHead(node: ProtoNode, currentSlot: Slot): boolean {
|
|
723
|
+
// If node has invalid executionStatus, it can't be a viable head
|
|
724
|
+
if (node.executionStatus === ExecutionStatus.Invalid) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
const currentEpoch = computeEpochAtSlot(currentSlot);
|
|
728
|
+
|
|
729
|
+
// If block is from a previous epoch, filter using unrealized justification & finalization information
|
|
730
|
+
// If block is from the current epoch, filter using the head state's justification & finalization information
|
|
731
|
+
const isFromPrevEpoch = computeEpochAtSlot(node.slot) < currentEpoch;
|
|
732
|
+
const votingSourceEpoch = isFromPrevEpoch ? node.unrealizedJustifiedEpoch : node.justifiedEpoch;
|
|
733
|
+
|
|
734
|
+
// The voting source should be at the same height as the store's justified checkpoint or
|
|
735
|
+
// not more than two epochs ago
|
|
736
|
+
const correctJustified =
|
|
737
|
+
this.justifiedEpoch === GENESIS_EPOCH ||
|
|
738
|
+
votingSourceEpoch === this.justifiedEpoch ||
|
|
739
|
+
votingSourceEpoch + 2 >= currentEpoch;
|
|
740
|
+
|
|
741
|
+
const correctFinalized = this.finalizedEpoch === 0 || this.isFinalizedRootOrDescendant(node);
|
|
742
|
+
return correctJustified && correctFinalized;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Return `true` if `node` is equal to or a descendant of the finalized node.
|
|
747
|
+
* This function helps improve performance of nodeIsViableForHead a lot by avoiding
|
|
748
|
+
* the loop inside `getAncestors`.
|
|
749
|
+
*/
|
|
750
|
+
isFinalizedRootOrDescendant(node: ProtoNode): boolean {
|
|
751
|
+
// The finalized and justified checkpoints represent a list of known
|
|
752
|
+
// ancestors of `node` that are likely to coincide with the store's
|
|
753
|
+
// finalized checkpoint.
|
|
754
|
+
if (
|
|
755
|
+
(node.finalizedEpoch === this.finalizedEpoch && node.finalizedRoot === this.finalizedRoot) ||
|
|
756
|
+
(node.justifiedEpoch === this.finalizedEpoch && node.justifiedRoot === this.finalizedRoot) ||
|
|
757
|
+
(node.unrealizedFinalizedEpoch === this.finalizedEpoch && node.unrealizedFinalizedRoot === this.finalizedRoot) ||
|
|
758
|
+
(node.unrealizedJustifiedEpoch === this.finalizedEpoch && node.unrealizedJustifiedRoot === this.finalizedRoot)
|
|
759
|
+
) {
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch);
|
|
764
|
+
return this.finalizedEpoch === 0 || this.finalizedRoot === this.getAncestorOrNull(node.blockRoot, finalizedSlot);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Same to getAncestor but it may return null instead of throwing error
|
|
769
|
+
*/
|
|
770
|
+
getAncestorOrNull(blockRoot: RootHex, ancestorSlot: Slot): RootHex | null {
|
|
771
|
+
try {
|
|
772
|
+
return this.getAncestor(blockRoot, ancestorSlot);
|
|
773
|
+
} catch (_) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Returns the block root of an ancestor of `blockRoot` at the given `slot`.
|
|
780
|
+
* (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
|
|
781
|
+
*
|
|
782
|
+
* NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
|
|
783
|
+
*
|
|
784
|
+
* ### Specification
|
|
785
|
+
*
|
|
786
|
+
* Equivalent to:
|
|
787
|
+
*
|
|
788
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_ancestor
|
|
789
|
+
*/
|
|
790
|
+
getAncestor(blockRoot: RootHex, ancestorSlot: Slot): RootHex {
|
|
791
|
+
const block = this.getBlock(blockRoot);
|
|
792
|
+
if (!block) {
|
|
793
|
+
throw new ForkChoiceError({
|
|
794
|
+
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
795
|
+
root: blockRoot,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (block.slot > ancestorSlot) {
|
|
800
|
+
// Search for a slot that is lte the target slot.
|
|
801
|
+
// We check for lower slots to account for skip slots.
|
|
802
|
+
for (const node of this.iterateAncestorNodes(blockRoot)) {
|
|
803
|
+
if (node.slot <= ancestorSlot) {
|
|
804
|
+
return node.blockRoot;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
throw new ForkChoiceError({
|
|
808
|
+
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
809
|
+
descendantRoot: blockRoot,
|
|
810
|
+
ancestorSlot,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// Root is older or equal than queried slot, thus a skip slot. Return most recent root prior to slot.
|
|
814
|
+
return blockRoot;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Iterate from a block root backwards over nodes
|
|
819
|
+
*/
|
|
820
|
+
*iterateAncestorNodes(blockRoot: RootHex): IterableIterator<ProtoNode> {
|
|
821
|
+
const startIndex = this.indices.get(blockRoot);
|
|
822
|
+
if (startIndex === undefined) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const node = this.nodes[startIndex];
|
|
827
|
+
if (node === undefined) {
|
|
828
|
+
throw new ProtoArrayError({
|
|
829
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
830
|
+
index: startIndex,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
yield* this.iterateAncestorNodesFromNode(node);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Iterate from a block root backwards over nodes
|
|
839
|
+
*/
|
|
840
|
+
*iterateAncestorNodesFromNode(node: ProtoNode): IterableIterator<ProtoNode> {
|
|
841
|
+
while (node.parent !== undefined) {
|
|
842
|
+
node = this.getNodeFromIndex(node.parent);
|
|
843
|
+
yield node;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Get all nodes from a block root backwards
|
|
849
|
+
*/
|
|
850
|
+
getAllAncestorNodes(blockRoot: RootHex): ProtoNode[] {
|
|
851
|
+
const startIndex = this.indices.get(blockRoot);
|
|
852
|
+
if (startIndex === undefined) {
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let node = this.nodes[startIndex];
|
|
857
|
+
if (node === undefined) {
|
|
858
|
+
throw new ProtoArrayError({
|
|
859
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
860
|
+
index: startIndex,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const nodes = [node];
|
|
865
|
+
|
|
866
|
+
while (node.parent !== undefined) {
|
|
867
|
+
node = this.getNodeFromIndex(node.parent);
|
|
868
|
+
nodes.push(node);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return nodes;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* The opposite of iterateNodes.
|
|
876
|
+
* iterateNodes is to find ancestor nodes of a blockRoot.
|
|
877
|
+
* this is to find non-ancestor nodes of a blockRoot.
|
|
878
|
+
*/
|
|
879
|
+
getAllNonAncestorNodes(blockRoot: RootHex): ProtoNode[] {
|
|
880
|
+
const startIndex = this.indices.get(blockRoot);
|
|
881
|
+
if (startIndex === undefined) {
|
|
882
|
+
return [];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
let node = this.nodes[startIndex];
|
|
886
|
+
if (node === undefined) {
|
|
887
|
+
throw new ProtoArrayError({
|
|
888
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
889
|
+
index: startIndex,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const result: ProtoNode[] = [];
|
|
893
|
+
let nodeIndex = startIndex;
|
|
894
|
+
while (node.parent !== undefined) {
|
|
895
|
+
const parentIndex = node.parent;
|
|
896
|
+
node = this.getNodeFromIndex(parentIndex);
|
|
897
|
+
// nodes between nodeIndex and parentIndex means non-ancestor nodes
|
|
898
|
+
result.push(...this.getNodesBetween(nodeIndex, parentIndex));
|
|
899
|
+
nodeIndex = parentIndex;
|
|
900
|
+
}
|
|
901
|
+
result.push(...this.getNodesBetween(nodeIndex, 0));
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Returns both ancestor and non-ancestor nodes in a single traversal.
|
|
907
|
+
*/
|
|
908
|
+
getAllAncestorAndNonAncestorNodes(blockRoot: RootHex): {ancestors: ProtoNode[]; nonAncestors: ProtoNode[]} {
|
|
909
|
+
const startIndex = this.indices.get(blockRoot);
|
|
910
|
+
if (startIndex === undefined) {
|
|
911
|
+
return {ancestors: [], nonAncestors: []};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
let node = this.nodes[startIndex];
|
|
915
|
+
if (node === undefined) {
|
|
916
|
+
throw new ProtoArrayError({
|
|
917
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
918
|
+
index: startIndex,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const ancestors: ProtoNode[] = [];
|
|
923
|
+
const nonAncestors: ProtoNode[] = [];
|
|
924
|
+
|
|
925
|
+
let nodeIndex = startIndex;
|
|
926
|
+
while (node.parent !== undefined) {
|
|
927
|
+
ancestors.push(node);
|
|
928
|
+
|
|
929
|
+
const parentIndex = node.parent;
|
|
930
|
+
node = this.getNodeFromIndex(parentIndex);
|
|
931
|
+
|
|
932
|
+
// Nodes between nodeIndex and parentIndex are non-ancestor nodes
|
|
933
|
+
nonAncestors.push(...this.getNodesBetween(nodeIndex, parentIndex));
|
|
934
|
+
nodeIndex = parentIndex;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
ancestors.push(node);
|
|
938
|
+
nonAncestors.push(...this.getNodesBetween(nodeIndex, 0));
|
|
939
|
+
|
|
940
|
+
return {ancestors, nonAncestors};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
hasBlock(blockRoot: RootHex): boolean {
|
|
944
|
+
return this.indices.has(blockRoot);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
getNode(blockRoot: RootHex): ProtoNode | undefined {
|
|
948
|
+
const blockIndex = this.indices.get(blockRoot);
|
|
949
|
+
if (blockIndex === undefined) {
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
return this.getNodeByIndex(blockIndex);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Return MUTABLE ProtoBlock for blockRoot (spreads properties) */
|
|
956
|
+
getBlock(blockRoot: RootHex): ProtoBlock | undefined {
|
|
957
|
+
const node = this.getNode(blockRoot);
|
|
958
|
+
if (!node) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
...node,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** Return NON-MUTABLE ProtoBlock for blockRoot (does not spread properties) */
|
|
967
|
+
getBlockReadonly(blockRoot: RootHex): ProtoBlock {
|
|
968
|
+
const node = this.getNode(blockRoot);
|
|
969
|
+
if (!node) {
|
|
970
|
+
throw Error(`No block for root ${blockRoot}`);
|
|
971
|
+
}
|
|
972
|
+
return node;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
977
|
+
* Always returns `false` if either input roots are unknown.
|
|
978
|
+
* Still returns `true` if `ancestorRoot` === `descendantRoot` (and the roots are known)
|
|
979
|
+
*/
|
|
980
|
+
isDescendant(ancestorRoot: RootHex, descendantRoot: RootHex): boolean {
|
|
981
|
+
const ancestorNode = this.getNode(ancestorRoot);
|
|
982
|
+
if (!ancestorNode) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (ancestorRoot === descendantRoot) {
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
for (const node of this.iterateAncestorNodes(descendantRoot)) {
|
|
991
|
+
if (node.slot < ancestorNode.slot) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (node.blockRoot === ancestorNode.blockRoot) {
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Returns a common ancestor for nodeA or nodeB or null if there's none
|
|
1003
|
+
*/
|
|
1004
|
+
getCommonAncestor(nodeA: ProtoNode, nodeB: ProtoNode): ProtoNode | null {
|
|
1005
|
+
while (true) {
|
|
1006
|
+
// If nodeA is higher than nodeB walk up nodeA tree
|
|
1007
|
+
if (nodeA.slot > nodeB.slot) {
|
|
1008
|
+
if (nodeA.parent === undefined) {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
nodeA = this.getNodeFromIndex(nodeA.parent);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// If nodeB is higher than nodeA walk up nodeB tree
|
|
1016
|
+
else if (nodeA.slot < nodeB.slot) {
|
|
1017
|
+
if (nodeB.parent === undefined) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
nodeB = this.getNodeFromIndex(nodeB.parent);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// If both node trees are at the same height, if same root == common ancestor.
|
|
1025
|
+
// Otherwise, keep walking up until there's a match or no parent.
|
|
1026
|
+
else {
|
|
1027
|
+
if (nodeA.blockRoot === nodeB.blockRoot) {
|
|
1028
|
+
return nodeA;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (nodeA.parent === undefined || nodeB.parent === undefined) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
nodeA = this.getNodeFromIndex(nodeA.parent);
|
|
1036
|
+
nodeB = this.getNodeFromIndex(nodeB.parent);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
length(): number {
|
|
1042
|
+
return this.indices.size;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private getNodeFromIndex(index: number): ProtoNode {
|
|
1046
|
+
const node = this.nodes[index];
|
|
1047
|
+
if (node === undefined) {
|
|
1048
|
+
throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index});
|
|
1049
|
+
}
|
|
1050
|
+
return node;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private getNodeByIndex(blockIndex: number): ProtoNode | undefined {
|
|
1054
|
+
const node = this.nodes[blockIndex];
|
|
1055
|
+
if (node === undefined) {
|
|
1056
|
+
return undefined;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return node;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private getNodesBetween(upperIndex: number, lowerIndex: number): ProtoNode[] {
|
|
1063
|
+
const result = [];
|
|
1064
|
+
for (let index = upperIndex - 1; index > lowerIndex; index--) {
|
|
1065
|
+
const node = this.nodes[index];
|
|
1066
|
+
if (node === undefined) {
|
|
1067
|
+
throw new ProtoArrayError({
|
|
1068
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
1069
|
+
index,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
result.push(node);
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
}
|