@lodestar/fork-choice 1.41.0-dev.6eb55025ce → 1.41.0-dev.8299317d72
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 +9 -1
- package/lib/forkChoice/errors.d.ts.map +1 -1
- package/lib/forkChoice/errors.js +10 -3
- package/lib/forkChoice/errors.js.map +1 -1
- package/lib/forkChoice/forkChoice.d.ts +76 -20
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +304 -118
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +54 -21
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js +6 -3
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/forkChoice/safeBlocks.js.map +1 -1
- package/lib/forkChoice/store.d.ts +40 -16
- package/lib/forkChoice/store.d.ts.map +1 -1
- package/lib/forkChoice/store.js +22 -4
- package/lib/forkChoice/store.js.map +1 -1
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js.map +1 -1
- package/lib/protoArray/computeDeltas.d.ts.map +1 -1
- package/lib/protoArray/computeDeltas.js +3 -0
- package/lib/protoArray/computeDeltas.js.map +1 -1
- package/lib/protoArray/errors.d.ts +15 -2
- package/lib/protoArray/errors.d.ts.map +1 -1
- package/lib/protoArray/errors.js +7 -2
- package/lib/protoArray/errors.js.map +1 -1
- package/lib/protoArray/interface.d.ts +33 -3
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/interface.js +31 -1
- package/lib/protoArray/interface.js.map +1 -1
- package/lib/protoArray/protoArray.d.ts +225 -24
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +763 -134
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +9 -9
- package/src/forkChoice/errors.ts +7 -2
- package/src/forkChoice/forkChoice.ts +387 -127
- package/src/forkChoice/interface.ts +72 -20
- package/src/forkChoice/store.ts +52 -20
- package/src/index.ts +10 -2
- package/src/protoArray/computeDeltas.ts +6 -0
- package/src/protoArray/errors.ts +7 -1
- package/src/protoArray/interface.ts +48 -3
- package/src/protoArray/protoArray.ts +896 -135
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BitArray } from "@chainsafe/ssz";
|
|
2
|
+
import { GENESIS_EPOCH, PTC_SIZE } from "@lodestar/params";
|
|
2
3
|
import { computeEpochAtSlot, computeStartSlotAtEpoch } from "@lodestar/state-transition";
|
|
3
|
-
import { toRootHex } from "@lodestar/utils";
|
|
4
|
+
import { bitCount, toRootHex } from "@lodestar/utils";
|
|
4
5
|
import { ForkChoiceError, ForkChoiceErrorCode } from "../forkChoice/errors.js";
|
|
5
6
|
import { LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode } from "./errors.js";
|
|
6
|
-
import { ExecutionStatus, HEX_ZERO_HASH } from "./interface.js";
|
|
7
|
+
import { ExecutionStatus, HEX_ZERO_HASH, PayloadStatus, isGloasBlock, } from "./interface.js";
|
|
8
|
+
/**
|
|
9
|
+
* Threshold for payload timeliness (>50% of PTC must vote)
|
|
10
|
+
* Spec: gloas/fork-choice.md (PAYLOAD_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
11
|
+
*/
|
|
12
|
+
const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
7
13
|
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
8
14
|
const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
|
|
9
15
|
export class ProtoArray {
|
|
@@ -15,9 +21,28 @@ export class ProtoArray {
|
|
|
15
21
|
finalizedEpoch;
|
|
16
22
|
finalizedRoot;
|
|
17
23
|
nodes = [];
|
|
24
|
+
/**
|
|
25
|
+
* Maps block root to array of node indices for each payload status variant
|
|
26
|
+
*
|
|
27
|
+
* Array structure: [PENDING, EMPTY, FULL] where indices correspond to PayloadStatus enum values
|
|
28
|
+
* - number[0] = PENDING variant index (PayloadStatus.PENDING = 0)
|
|
29
|
+
* - number[1] = EMPTY variant index (PayloadStatus.EMPTY = 1)
|
|
30
|
+
* - number[2] = FULL variant index (PayloadStatus.FULL = 2)
|
|
31
|
+
*
|
|
32
|
+
* Note: undefined array elements indicate that variant doesn't exist for this block
|
|
33
|
+
*/
|
|
18
34
|
indices = new Map();
|
|
19
35
|
lvhError;
|
|
20
36
|
previousProposerBoost = null;
|
|
37
|
+
/**
|
|
38
|
+
* PTC (Payload Timeliness Committee) votes per block as bitvectors
|
|
39
|
+
* Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal)
|
|
40
|
+
* Spec: gloas/fork-choice.md#modified-store (line 148)
|
|
41
|
+
*
|
|
42
|
+
* Bit i is set if PTC member i voted payload_present=true
|
|
43
|
+
* Used by is_payload_timely() to determine if payload is timely
|
|
44
|
+
*/
|
|
45
|
+
ptcVotes = new Map();
|
|
21
46
|
constructor({ pruneThreshold, justifiedEpoch, justifiedRoot, finalizedEpoch, finalizedRoot, }) {
|
|
22
47
|
this.pruneThreshold = pruneThreshold;
|
|
23
48
|
this.justifiedEpoch = justifiedEpoch;
|
|
@@ -37,9 +62,163 @@ export class ProtoArray {
|
|
|
37
62
|
...block,
|
|
38
63
|
// We are using the blockROot as the targetRoot, since it always lies on an epoch boundary
|
|
39
64
|
targetRoot: block.blockRoot,
|
|
40
|
-
}, currentSlot);
|
|
65
|
+
}, currentSlot, null);
|
|
41
66
|
return protoArray;
|
|
42
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Get node index for a block root and payload status
|
|
70
|
+
*
|
|
71
|
+
* @param root - The block root to look up
|
|
72
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
73
|
+
* @returns The node index for the specified variant, or undefined if not found
|
|
74
|
+
*
|
|
75
|
+
* Behavior:
|
|
76
|
+
* - Pre-Gloas blocks: only FULL is valid, PENDING/EMPTY throw error
|
|
77
|
+
* - Gloas blocks: returns the specified variant index, or undefined if that variant doesn't exist
|
|
78
|
+
*
|
|
79
|
+
* Note: payloadStatus is required. Use getDefaultVariant() to get the canonical variant.
|
|
80
|
+
*/
|
|
81
|
+
getNodeIndexByRootAndStatus(root, payloadStatus) {
|
|
82
|
+
const variantOrArr = this.indices.get(root);
|
|
83
|
+
if (variantOrArr == null) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
// Pre-Gloas: only FULL variant exists
|
|
87
|
+
if (!Array.isArray(variantOrArr)) {
|
|
88
|
+
// Return FULL variant if no status specified or FULL explicitly requested
|
|
89
|
+
if (payloadStatus === PayloadStatus.FULL) {
|
|
90
|
+
return variantOrArr;
|
|
91
|
+
}
|
|
92
|
+
// PENDING and EMPTY are invalid for pre-Gloas blocks
|
|
93
|
+
throw new ProtoArrayError({
|
|
94
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
95
|
+
index: payloadStatus,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Gloas: return the specified variant, or PENDING if not specified
|
|
99
|
+
return variantOrArr[payloadStatus];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the default/canonical payload status for a block root
|
|
103
|
+
* - Pre-Gloas blocks: Returns FULL (payload embedded in block)
|
|
104
|
+
* - Gloas blocks: Returns PENDING (canonical variant)
|
|
105
|
+
*
|
|
106
|
+
* @param blockRoot - The block root to check
|
|
107
|
+
* @returns PayloadStatus.FULL for pre-Gloas, PayloadStatus.PENDING for Gloas, undefined if block not found
|
|
108
|
+
*/
|
|
109
|
+
getDefaultVariant(blockRoot) {
|
|
110
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
111
|
+
if (variantOrArr == null) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// Pre-Gloas: only FULL variant exists
|
|
115
|
+
if (!Array.isArray(variantOrArr)) {
|
|
116
|
+
return PayloadStatus.FULL;
|
|
117
|
+
}
|
|
118
|
+
// Gloas: multiple variants exist, PENDING is canonical
|
|
119
|
+
return PayloadStatus.PENDING;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the node index for the default/canonical variant in a single hash lookup.
|
|
123
|
+
* - Pre-Gloas blocks: returns the FULL variant index
|
|
124
|
+
* - Gloas blocks: returns the PENDING variant index
|
|
125
|
+
*/
|
|
126
|
+
getDefaultNodeIndex(blockRoot) {
|
|
127
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
128
|
+
if (variantOrArr == null) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
// Pre-Gloas: value is the index directly
|
|
132
|
+
if (!Array.isArray(variantOrArr)) {
|
|
133
|
+
return variantOrArr;
|
|
134
|
+
}
|
|
135
|
+
// Gloas: PENDING is the canonical variant
|
|
136
|
+
return variantOrArr[PayloadStatus.PENDING];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Determine which parent payload status a block extends
|
|
140
|
+
* Spec: gloas/fork-choice.md#new-get_parent_payload_status
|
|
141
|
+
* def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
|
|
142
|
+
* parent = store.blocks[block.parent_root]
|
|
143
|
+
* parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
|
|
144
|
+
* message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
|
|
145
|
+
* return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY
|
|
146
|
+
*
|
|
147
|
+
* In lodestar forkchoice, we don't store the full bid, so we compares parent_block_hash in child's bid with executionPayloadBlockHash in parent:
|
|
148
|
+
* - If it matches EMPTY variant, return EMPTY
|
|
149
|
+
* - If it matches FULL variant, return FULL
|
|
150
|
+
* - If no match, throw UNKNOWN_PARENT_BLOCK error
|
|
151
|
+
*
|
|
152
|
+
* For pre-Gloas blocks: always returns FULL
|
|
153
|
+
*/
|
|
154
|
+
getParentPayloadStatus(block) {
|
|
155
|
+
// Pre-Gloas blocks have payloads embedded, so parents are always FULL
|
|
156
|
+
const { parentBlockHash } = block;
|
|
157
|
+
if (parentBlockHash === null) {
|
|
158
|
+
return PayloadStatus.FULL;
|
|
159
|
+
}
|
|
160
|
+
const parentBlock = this.getBlockHexAndBlockHash(block.parentRoot, parentBlockHash);
|
|
161
|
+
if (parentBlock == null) {
|
|
162
|
+
throw new ProtoArrayError({
|
|
163
|
+
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
|
|
164
|
+
parentRoot: block.parentRoot,
|
|
165
|
+
parentHash: parentBlockHash,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return parentBlock.payloadStatus;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Return the parent `ProtoBlock` given its root and block hash.
|
|
172
|
+
*/
|
|
173
|
+
getParent(parentRoot, parentBlockHash) {
|
|
174
|
+
// pre-gloas
|
|
175
|
+
if (parentBlockHash === null) {
|
|
176
|
+
const parentIndex = this.indices.get(parentRoot);
|
|
177
|
+
if (parentIndex === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(parentIndex)) {
|
|
181
|
+
// Gloas block found when pre-gloas expected
|
|
182
|
+
throw new ProtoArrayError({
|
|
183
|
+
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
|
|
184
|
+
parentRoot,
|
|
185
|
+
parentHash: parentBlockHash,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return this.nodes[parentIndex] ?? null;
|
|
189
|
+
}
|
|
190
|
+
// post-gloas
|
|
191
|
+
return this.getBlockHexAndBlockHash(parentRoot, parentBlockHash);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
|
|
195
|
+
*/
|
|
196
|
+
getBlockHexAndBlockHash(blockRoot, blockHash) {
|
|
197
|
+
const variantIndices = this.indices.get(blockRoot);
|
|
198
|
+
if (variantIndices === undefined) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// Pre-Gloas
|
|
202
|
+
if (!Array.isArray(variantIndices)) {
|
|
203
|
+
const node = this.nodes[variantIndices];
|
|
204
|
+
return node.executionPayloadBlockHash === blockHash ? node : null;
|
|
205
|
+
}
|
|
206
|
+
// Post-Gloas, check empty and full variants
|
|
207
|
+
const fullNodeIndex = variantIndices[PayloadStatus.FULL];
|
|
208
|
+
if (fullNodeIndex !== undefined) {
|
|
209
|
+
const fullNode = this.nodes[fullNodeIndex];
|
|
210
|
+
if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
|
|
211
|
+
return fullNode;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
|
|
215
|
+
if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
|
|
216
|
+
return emptyNode;
|
|
217
|
+
}
|
|
218
|
+
// PENDING is the same to EMPTY so not likely we can return it
|
|
219
|
+
// also it's only specific for fork-choice
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
43
222
|
/**
|
|
44
223
|
* Iterate backwards through the array, touching all nodes and their parents and potentially
|
|
45
224
|
* the best-child of each parent.
|
|
@@ -56,11 +235,11 @@ export class ProtoArray {
|
|
|
56
235
|
* - If required, update the parents best-descendant with the current node or its best-descendant.
|
|
57
236
|
*/
|
|
58
237
|
applyScoreChanges({ deltas, proposerBoost, justifiedEpoch, justifiedRoot, finalizedEpoch, finalizedRoot, currentSlot, }) {
|
|
59
|
-
if (deltas.length !== this.
|
|
238
|
+
if (deltas.length !== this.nodes.length) {
|
|
60
239
|
throw new ProtoArrayError({
|
|
61
240
|
code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
|
|
62
241
|
deltas: deltas.length,
|
|
63
|
-
indices: this.
|
|
242
|
+
indices: this.nodes.length,
|
|
64
243
|
});
|
|
65
244
|
}
|
|
66
245
|
if (justifiedEpoch !== this.justifiedEpoch ||
|
|
@@ -129,7 +308,7 @@ export class ProtoArray {
|
|
|
129
308
|
// If the node has a parent, try to update its best-child and best-descendant.
|
|
130
309
|
const parentIndex = node.parent;
|
|
131
310
|
if (parentIndex !== undefined) {
|
|
132
|
-
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot);
|
|
311
|
+
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, proposerBoost?.root ?? null);
|
|
133
312
|
}
|
|
134
313
|
}
|
|
135
314
|
// Update the previous proposer boost
|
|
@@ -140,9 +319,9 @@ export class ProtoArray {
|
|
|
140
319
|
*
|
|
141
320
|
* It is only sane to supply an undefined parent for the genesis block
|
|
142
321
|
*/
|
|
143
|
-
onBlock(block, currentSlot) {
|
|
322
|
+
onBlock(block, currentSlot, proposerBoostRoot) {
|
|
144
323
|
// If the block is already known, simply ignore it
|
|
145
|
-
if (this.
|
|
324
|
+
if (this.hasBlock(block.blockRoot)) {
|
|
146
325
|
return;
|
|
147
326
|
}
|
|
148
327
|
if (block.executionStatus === ExecutionStatus.Invalid) {
|
|
@@ -151,30 +330,257 @@ export class ProtoArray {
|
|
|
151
330
|
root: block.blockRoot,
|
|
152
331
|
});
|
|
153
332
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
parent
|
|
333
|
+
if (isGloasBlock(block)) {
|
|
334
|
+
// Gloas: Create PENDING + EMPTY nodes with correct parent relationships
|
|
335
|
+
// Parent of new PENDING node = parent block's EMPTY or FULL (inter-block edge)
|
|
336
|
+
// Parent of new EMPTY node = own PENDING node (intra-block edge)
|
|
337
|
+
// For fork transition: if parent is pre-Gloas, point to parent's FULL
|
|
338
|
+
// Otherwise, determine which parent payload status this block extends
|
|
339
|
+
let parentIndex;
|
|
340
|
+
// Check if parent exists by getting variants array
|
|
341
|
+
const parentVariants = this.indices.get(block.parentRoot);
|
|
342
|
+
if (parentVariants != null) {
|
|
343
|
+
const anyParentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
|
|
344
|
+
const anyParentNode = this.nodes[anyParentIndex];
|
|
345
|
+
if (!isGloasBlock(anyParentNode)) {
|
|
346
|
+
// Fork transition: parent is pre-Gloas, so it only has FULL variant at variants[0]
|
|
347
|
+
parentIndex = anyParentIndex;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Both blocks are Gloas: determine which parent payload status to extend
|
|
351
|
+
const parentPayloadStatus = this.getParentPayloadStatus(block);
|
|
352
|
+
parentIndex = this.getNodeIndexByRootAndStatus(block.parentRoot, parentPayloadStatus);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// else: parent doesn't exist, parentIndex remains undefined (orphan block)
|
|
356
|
+
// Create PENDING node
|
|
357
|
+
const pendingNode = {
|
|
358
|
+
...block,
|
|
359
|
+
parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
|
|
360
|
+
payloadStatus: PayloadStatus.PENDING,
|
|
361
|
+
weight: 0,
|
|
362
|
+
bestChild: undefined,
|
|
363
|
+
bestDescendant: undefined,
|
|
364
|
+
};
|
|
365
|
+
const pendingIndex = this.nodes.length;
|
|
366
|
+
this.nodes.push(pendingNode);
|
|
367
|
+
// Create EMPTY variant as a child of PENDING
|
|
368
|
+
const emptyNode = {
|
|
369
|
+
...block,
|
|
370
|
+
parent: pendingIndex, // Points to own PENDING
|
|
371
|
+
payloadStatus: PayloadStatus.EMPTY,
|
|
372
|
+
weight: 0,
|
|
373
|
+
bestChild: undefined,
|
|
374
|
+
bestDescendant: undefined,
|
|
375
|
+
};
|
|
376
|
+
const emptyIndex = this.nodes.length;
|
|
377
|
+
this.nodes.push(emptyNode);
|
|
378
|
+
// Store both variants in the indices array
|
|
379
|
+
// [PENDING, EMPTY, undefined] - FULL will be added later if payload arrives
|
|
380
|
+
this.indices.set(block.blockRoot, [pendingIndex, emptyIndex]);
|
|
381
|
+
// Update bestChild pointers
|
|
382
|
+
if (parentIndex !== undefined) {
|
|
383
|
+
this.maybeUpdateBestChildAndDescendant(parentIndex, pendingIndex, currentSlot, proposerBoostRoot);
|
|
384
|
+
if (pendingNode.executionStatus === ExecutionStatus.Valid) {
|
|
385
|
+
this.propagateValidExecutionStatusByIndex(parentIndex);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Update bestChild for PENDING → EMPTY edge
|
|
389
|
+
this.maybeUpdateBestChildAndDescendant(pendingIndex, emptyIndex, currentSlot, proposerBoostRoot);
|
|
390
|
+
// Initialize PTC votes for this block (all false initially)
|
|
391
|
+
// Spec: gloas/fork-choice.md#modified-on_block (line 645)
|
|
392
|
+
this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Pre-Gloas: Only create FULL node (payload embedded in block)
|
|
396
|
+
const node = {
|
|
397
|
+
...block,
|
|
398
|
+
parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
|
|
399
|
+
payloadStatus: PayloadStatus.FULL,
|
|
400
|
+
weight: 0,
|
|
401
|
+
bestChild: undefined,
|
|
402
|
+
bestDescendant: undefined,
|
|
403
|
+
};
|
|
404
|
+
const nodeIndex = this.nodes.length;
|
|
405
|
+
this.nodes.push(node);
|
|
406
|
+
// Pre-Gloas: store FULL index instead of array
|
|
407
|
+
this.indices.set(block.blockRoot, nodeIndex);
|
|
408
|
+
// If this node is valid, lets propagate the valid status up the chain
|
|
409
|
+
// and throw error if we counter invalid, as this breaks consensus
|
|
410
|
+
if (node.parent !== undefined) {
|
|
411
|
+
this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot, proposerBoostRoot);
|
|
412
|
+
if (node.executionStatus === ExecutionStatus.Valid) {
|
|
413
|
+
this.propagateValidExecutionStatusByIndex(node.parent);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Called when an execution payload is received for a block (Gloas only)
|
|
420
|
+
* Creates a FULL variant node as a sibling to the existing EMPTY variant
|
|
421
|
+
* Both EMPTY and FULL have parent = own PENDING node
|
|
422
|
+
*
|
|
423
|
+
* Spec: gloas/fork-choice.md (on_execution_payload event)
|
|
424
|
+
*/
|
|
425
|
+
onExecutionPayload(blockRoot, currentSlot, executionPayloadBlockHash, executionPayloadNumber, executionPayloadStateRoot, proposerBoostRoot) {
|
|
426
|
+
// First check if block exists
|
|
427
|
+
const variants = this.indices.get(blockRoot);
|
|
428
|
+
if (variants == null) {
|
|
429
|
+
// Equivalent to `assert envelope.beacon_block_root in store.block_states`
|
|
430
|
+
throw new ProtoArrayError({
|
|
431
|
+
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
|
|
432
|
+
root: blockRoot,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (!Array.isArray(variants)) {
|
|
436
|
+
// Pre-gloas block should not be calling this method
|
|
437
|
+
throw new ProtoArrayError({
|
|
438
|
+
code: ProtoArrayErrorCode.PRE_GLOAS_BLOCK,
|
|
439
|
+
root: blockRoot,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// Check if FULL already exists for Gloas blocks
|
|
443
|
+
if (variants[PayloadStatus.FULL] !== undefined) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Get PENDING node for Gloas blocks
|
|
447
|
+
const pendingIndex = variants[PayloadStatus.PENDING];
|
|
448
|
+
if (pendingIndex === undefined) {
|
|
449
|
+
throw new ProtoArrayError({
|
|
450
|
+
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
|
|
451
|
+
root: blockRoot,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const pendingNode = this.nodes[pendingIndex];
|
|
455
|
+
if (!pendingNode) {
|
|
456
|
+
throw new ProtoArrayError({
|
|
457
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
458
|
+
index: pendingIndex,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
// Create FULL variant as a child of PENDING (sibling to EMPTY)
|
|
462
|
+
const fullNode = {
|
|
463
|
+
...pendingNode,
|
|
464
|
+
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
465
|
+
payloadStatus: PayloadStatus.FULL,
|
|
157
466
|
weight: 0,
|
|
158
467
|
bestChild: undefined,
|
|
159
468
|
bestDescendant: undefined,
|
|
469
|
+
executionStatus: ExecutionStatus.Valid,
|
|
470
|
+
executionPayloadBlockHash,
|
|
471
|
+
executionPayloadNumber,
|
|
472
|
+
stateRoot: executionPayloadStateRoot,
|
|
160
473
|
};
|
|
161
|
-
const
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
474
|
+
const fullIndex = this.nodes.length;
|
|
475
|
+
this.nodes.push(fullNode);
|
|
476
|
+
// Add FULL variant to the indices array
|
|
477
|
+
variants[PayloadStatus.FULL] = fullIndex;
|
|
478
|
+
// Update bestChild for PENDING node (may now prefer FULL over EMPTY)
|
|
479
|
+
this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Update PTC votes for multiple validators attesting to a block
|
|
483
|
+
* Spec: gloas/fork-choice.md#new-on_payload_attestation_message
|
|
484
|
+
*
|
|
485
|
+
* @param blockRoot - The beacon block root being attested
|
|
486
|
+
* @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1)
|
|
487
|
+
* @param payloadPresent - Whether the validators attest the payload is present
|
|
488
|
+
*/
|
|
489
|
+
notifyPtcMessages(blockRoot, ptcIndices, payloadPresent) {
|
|
490
|
+
const votes = this.ptcVotes.get(blockRoot);
|
|
491
|
+
if (votes === undefined) {
|
|
492
|
+
// Block not found or not a Gloas block, ignore
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
for (const ptcIndex of ptcIndices) {
|
|
496
|
+
if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
|
|
497
|
+
throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
|
|
170
498
|
}
|
|
499
|
+
votes.set(ptcIndex, payloadPresent);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check if execution payload for a block is timely
|
|
504
|
+
* Spec: gloas/fork-choice.md#new-is_payload_timely
|
|
505
|
+
*
|
|
506
|
+
* Returns true if:
|
|
507
|
+
* 1. Block has PTC votes tracked
|
|
508
|
+
* 2. Payload is locally available (FULL variant exists in proto array)
|
|
509
|
+
* 3. More than PAYLOAD_TIMELY_THRESHOLD (>50% of PTC) members voted payload_present=true
|
|
510
|
+
*
|
|
511
|
+
* @param blockRoot - The beacon block root to check
|
|
512
|
+
*/
|
|
513
|
+
isPayloadTimely(blockRoot) {
|
|
514
|
+
const votes = this.ptcVotes.get(blockRoot);
|
|
515
|
+
if (votes === undefined) {
|
|
516
|
+
// Block not found or not a Gloas block
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
// If payload is not locally available, it's not timely
|
|
520
|
+
// In our implementation, payload is locally available if proto array has FULL variant of the block
|
|
521
|
+
const fullNodeIndex = this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL);
|
|
522
|
+
if (fullNodeIndex === undefined) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
// Count votes for payload_present=true
|
|
526
|
+
const yesVotes = bitCount(votes.uint8Array);
|
|
527
|
+
return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Check if parent node is FULL
|
|
531
|
+
* Spec: gloas/fork-choice.md#new-is_parent_node_full
|
|
532
|
+
*
|
|
533
|
+
* Returns true if the parent payload status (determined by block.parentBlockHash) is FULL
|
|
534
|
+
*/
|
|
535
|
+
isParentNodeFull(block) {
|
|
536
|
+
return this.getParentPayloadStatus(block) === PayloadStatus.FULL;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Determine if we should extend the payload (prefer FULL over EMPTY)
|
|
540
|
+
* Spec: gloas/fork-choice.md#new-should_extend_payload
|
|
541
|
+
*
|
|
542
|
+
* Returns true if:
|
|
543
|
+
* 1. Payload is timely, OR
|
|
544
|
+
* 2. No proposer boost root (empty/zero hash), OR
|
|
545
|
+
* 3. Proposer boost root's parent is not this block, OR
|
|
546
|
+
* 4. Proposer boost root extends FULL parent
|
|
547
|
+
*
|
|
548
|
+
* @param blockRoot - The block root to check
|
|
549
|
+
* @param proposerBoostRoot - Current proposer boost root (from ForkChoice)
|
|
550
|
+
*/
|
|
551
|
+
shouldExtendPayload(blockRoot, proposerBoostRoot) {
|
|
552
|
+
// Condition 1: Payload is timely
|
|
553
|
+
if (this.isPayloadTimely(blockRoot)) {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
// Condition 2: No proposer boost root
|
|
557
|
+
if (proposerBoostRoot === null || proposerBoostRoot === HEX_ZERO_HASH) {
|
|
558
|
+
return true;
|
|
171
559
|
}
|
|
560
|
+
// Get proposer boost block
|
|
561
|
+
// We don't care about variant here, just need proposer boost block info
|
|
562
|
+
const proposerBoostIndex = this.getDefaultNodeIndex(proposerBoostRoot);
|
|
563
|
+
const proposerBoostBlock = proposerBoostIndex !== undefined ? this.getNodeByIndex(proposerBoostIndex) : undefined;
|
|
564
|
+
if (!proposerBoostBlock) {
|
|
565
|
+
// Proposer boost block not found, default to extending payload
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
// Condition 3: Proposer boost root's parent is not this block
|
|
569
|
+
if (proposerBoostBlock.parentRoot !== blockRoot) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
// Condition 4: Proposer boost root extends FULL parent
|
|
573
|
+
if (this.isParentNodeFull(proposerBoostBlock)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
return false;
|
|
172
577
|
}
|
|
173
578
|
/**
|
|
174
579
|
* Optimistic sync validate till validated latest hash, invalidate any descendant branch
|
|
175
580
|
* if invalidate till hash provided. If consensus fails, this will invalidate entire
|
|
176
581
|
* forkChoice which will throw on any call to findHead
|
|
177
582
|
*/
|
|
583
|
+
// TODO GLOAS: Review usage of this post-gloas
|
|
178
584
|
validateLatestHash(execResponse, currentSlot) {
|
|
179
585
|
// Look reverse because its highly likely node with latestValidExecHash is towards the
|
|
180
586
|
// the leaves of the forkchoice
|
|
@@ -216,7 +622,8 @@ export class ProtoArray {
|
|
|
216
622
|
// if its in fcU.
|
|
217
623
|
//
|
|
218
624
|
const { invalidateFromParentBlockRoot, latestValidExecHash } = execResponse;
|
|
219
|
-
|
|
625
|
+
// TODO GLOAS: verify if getting the default/canonical node index is correct here
|
|
626
|
+
const invalidateFromParentIndex = this.getDefaultNodeIndex(invalidateFromParentBlockRoot);
|
|
220
627
|
if (invalidateFromParentIndex === undefined) {
|
|
221
628
|
throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
|
|
222
629
|
}
|
|
@@ -251,6 +658,12 @@ export class ProtoArray {
|
|
|
251
658
|
if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
|
|
252
659
|
break;
|
|
253
660
|
}
|
|
661
|
+
// If PayloadSeparated, that means the node is either PENDING or EMPTY, there could be
|
|
662
|
+
// some ancestor still has syncing status.
|
|
663
|
+
if (node.executionStatus === ExecutionStatus.PayloadSeparated) {
|
|
664
|
+
nodeIndex = node.parent;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
254
667
|
this.validateNodeByIndex(nodeIndex);
|
|
255
668
|
nodeIndex = node.parent;
|
|
256
669
|
}
|
|
@@ -350,8 +763,41 @@ export class ProtoArray {
|
|
|
350
763
|
}
|
|
351
764
|
return validNode;
|
|
352
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* Get payload status tiebreaker for fork choice comparison
|
|
768
|
+
* Spec: gloas/fork-choice.md#new-get_payload_status_tiebreaker
|
|
769
|
+
*
|
|
770
|
+
* For PENDING nodes: always returns 0
|
|
771
|
+
* For EMPTY/FULL variants from slot n-1: implements tiebreaker logic based on should_extend_payload
|
|
772
|
+
* For older blocks: returns node.payloadStatus
|
|
773
|
+
*
|
|
774
|
+
* Note: pre-gloas logic won't reach here. Pre-Gloas blocks have different roots, so they are always resolved by the weight and root tiebreaker before reaching here.
|
|
775
|
+
*/
|
|
776
|
+
getPayloadStatusTiebreaker(node, currentSlot, proposerBoostRoot) {
|
|
777
|
+
// PENDING nodes always return PENDING (no tiebreaker needed)
|
|
778
|
+
// PENDING=0, EMPTY=1, FULL=2
|
|
779
|
+
if (node.payloadStatus === PayloadStatus.PENDING) {
|
|
780
|
+
return node.payloadStatus;
|
|
781
|
+
}
|
|
782
|
+
// For Gloas: check if from previous slot
|
|
783
|
+
if (node.slot + 1 !== currentSlot) {
|
|
784
|
+
return node.payloadStatus;
|
|
785
|
+
}
|
|
786
|
+
// For previous slot blocks in Gloas, decide between FULL and EMPTY
|
|
787
|
+
// based on should_extend_payload
|
|
788
|
+
if (node.payloadStatus === PayloadStatus.EMPTY) {
|
|
789
|
+
return PayloadStatus.EMPTY;
|
|
790
|
+
}
|
|
791
|
+
// FULL - check should_extend_payload
|
|
792
|
+
const shouldExtend = this.shouldExtendPayload(node.blockRoot, proposerBoostRoot);
|
|
793
|
+
return shouldExtend ? PayloadStatus.FULL : PayloadStatus.PENDING;
|
|
794
|
+
}
|
|
353
795
|
/**
|
|
354
796
|
* Follows the best-descendant links to find the best-block (i.e., head-block).
|
|
797
|
+
*
|
|
798
|
+
* Returns the ProtoNode representing the head.
|
|
799
|
+
* For pre-Gloas forks, only FULL variants exist (payload embedded).
|
|
800
|
+
* For Gloas, may return PENDING/EMPTY/FULL variants.
|
|
355
801
|
*/
|
|
356
802
|
findHead(justifiedRoot, currentSlot) {
|
|
357
803
|
if (this.lvhError) {
|
|
@@ -360,7 +806,8 @@ export class ProtoArray {
|
|
|
360
806
|
...this.lvhError,
|
|
361
807
|
});
|
|
362
808
|
}
|
|
363
|
-
|
|
809
|
+
// Get canonical node: FULL for pre-Gloas, PENDING for Gloas
|
|
810
|
+
const justifiedIndex = this.getDefaultNodeIndex(justifiedRoot);
|
|
364
811
|
if (justifiedIndex === undefined) {
|
|
365
812
|
throw new ProtoArrayError({
|
|
366
813
|
code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN,
|
|
@@ -406,7 +853,7 @@ export class ProtoArray {
|
|
|
406
853
|
headFinalizedEpoch: justifiedNode.finalizedEpoch,
|
|
407
854
|
});
|
|
408
855
|
}
|
|
409
|
-
return bestNode
|
|
856
|
+
return bestNode;
|
|
410
857
|
}
|
|
411
858
|
/**
|
|
412
859
|
* Update the tree with new finalization information. The tree is only actually pruned if both
|
|
@@ -424,38 +871,68 @@ export class ProtoArray {
|
|
|
424
871
|
* - There is some internal error relating to invalid indices inside `this`.
|
|
425
872
|
*/
|
|
426
873
|
maybePrune(finalizedRoot) {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
874
|
+
const variants = this.indices.get(finalizedRoot);
|
|
875
|
+
if (variants == null) {
|
|
429
876
|
throw new ProtoArrayError({
|
|
430
877
|
code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN,
|
|
431
878
|
root: finalizedRoot,
|
|
432
879
|
});
|
|
433
880
|
}
|
|
881
|
+
// Find the minimum index among all variants to ensure we don't prune too much
|
|
882
|
+
const finalizedIndex = Array.isArray(variants)
|
|
883
|
+
? Math.min(...variants.filter((idx) => idx !== undefined))
|
|
884
|
+
: variants;
|
|
434
885
|
if (finalizedIndex < this.pruneThreshold) {
|
|
435
886
|
// Pruning at small numbers incurs more cost than benefit
|
|
436
887
|
return [];
|
|
437
888
|
}
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
889
|
+
// Collect all block roots that will be pruned
|
|
890
|
+
const prunedRoots = new Set();
|
|
891
|
+
for (let i = 0; i < finalizedIndex; i++) {
|
|
892
|
+
const node = this.nodes[i];
|
|
441
893
|
if (node === undefined) {
|
|
442
|
-
throw new ProtoArrayError({ code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index:
|
|
894
|
+
throw new ProtoArrayError({ code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index: i });
|
|
443
895
|
}
|
|
444
|
-
|
|
896
|
+
prunedRoots.add(node.blockRoot);
|
|
897
|
+
}
|
|
898
|
+
// Remove indices for pruned blocks and PTC votes
|
|
899
|
+
for (const root of prunedRoots) {
|
|
900
|
+
this.indices.delete(root);
|
|
901
|
+
// Prune PTC votes for this block to prevent memory leak
|
|
902
|
+
// Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
|
|
903
|
+
this.ptcVotes.delete(root);
|
|
445
904
|
}
|
|
446
905
|
// Store nodes prior to finalization
|
|
447
906
|
const removed = this.nodes.slice(0, finalizedIndex);
|
|
448
907
|
// Drop all the nodes prior to finalization
|
|
449
908
|
this.nodes = this.nodes.slice(finalizedIndex);
|
|
450
|
-
// Adjust the indices map
|
|
451
|
-
for (const [
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
909
|
+
// Adjust the indices map - subtract finalizedIndex from all node indices
|
|
910
|
+
for (const [root, variantIndices] of this.indices.entries()) {
|
|
911
|
+
// Pre-Gloas: single index
|
|
912
|
+
if (!Array.isArray(variantIndices)) {
|
|
913
|
+
if (variantIndices < finalizedIndex) {
|
|
914
|
+
throw new ProtoArrayError({
|
|
915
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
916
|
+
value: "indices",
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
this.indices.set(root, variantIndices - finalizedIndex);
|
|
920
|
+
continue;
|
|
457
921
|
}
|
|
458
|
-
|
|
922
|
+
// Post-Gloas: array of variant indices
|
|
923
|
+
const adjustedVariants = variantIndices.map((variantIndex) => {
|
|
924
|
+
if (variantIndex === undefined) {
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
if (variantIndex < finalizedIndex) {
|
|
928
|
+
throw new ProtoArrayError({
|
|
929
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
930
|
+
value: "indices",
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return variantIndex - finalizedIndex;
|
|
934
|
+
});
|
|
935
|
+
this.indices.set(root, adjustedVariants);
|
|
459
936
|
}
|
|
460
937
|
// Iterate through all the existing nodes and adjust their indices to match the new layout of this.nodes
|
|
461
938
|
for (let i = 0, len = this.nodes.length; i < len; i++) {
|
|
@@ -502,7 +979,7 @@ export class ProtoArray {
|
|
|
502
979
|
* - The child is not the best child but becomes the best child.
|
|
503
980
|
* - The child is not the best child and does not become the best child.
|
|
504
981
|
*/
|
|
505
|
-
maybeUpdateBestChildAndDescendant(parentIndex, childIndex, currentSlot) {
|
|
982
|
+
maybeUpdateBestChildAndDescendant(parentIndex, childIndex, currentSlot, proposerBoostRoot) {
|
|
506
983
|
const childNode = this.nodes[childIndex];
|
|
507
984
|
if (childNode === undefined) {
|
|
508
985
|
throw new ProtoArrayError({
|
|
@@ -523,61 +1000,87 @@ export class ProtoArray {
|
|
|
523
1000
|
const noChange = [parentNode.bestChild, parentNode.bestDescendant];
|
|
524
1001
|
let newChildAndDescendant;
|
|
525
1002
|
const bestChildIndex = parentNode.bestChild;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
// the child is the best-child already
|
|
534
|
-
// set it again to ensure that the best-descendent of the parent is updated
|
|
535
|
-
newChildAndDescendant = changeToChild;
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
const bestChildNode = this.nodes[bestChildIndex];
|
|
539
|
-
if (bestChildNode === undefined) {
|
|
540
|
-
throw new ProtoArrayError({
|
|
541
|
-
code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX,
|
|
542
|
-
index: bestChildIndex,
|
|
543
|
-
});
|
|
1003
|
+
// biome-ignore lint/suspicious/noConfusingLabels: labeled block used for early exit from complex decision tree
|
|
1004
|
+
outer: {
|
|
1005
|
+
if (bestChildIndex !== undefined) {
|
|
1006
|
+
if (bestChildIndex === childIndex && !childLeadsToViableHead) {
|
|
1007
|
+
// the child is already the best-child of the parent but its not viable for the head
|
|
1008
|
+
// so remove it
|
|
1009
|
+
newChildAndDescendant = changeToNull;
|
|
544
1010
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
//
|
|
1011
|
+
else if (bestChildIndex === childIndex) {
|
|
1012
|
+
// the child is the best-child already
|
|
1013
|
+
// set it again to ensure that the best-descendent of the parent is updated
|
|
548
1014
|
newChildAndDescendant = changeToChild;
|
|
549
1015
|
}
|
|
550
|
-
else
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1016
|
+
else {
|
|
1017
|
+
const bestChildNode = this.nodes[bestChildIndex];
|
|
1018
|
+
if (bestChildNode === undefined) {
|
|
1019
|
+
throw new ProtoArrayError({
|
|
1020
|
+
code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX,
|
|
1021
|
+
index: bestChildIndex,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
const bestChildLeadsToViableHead = this.nodeLeadsToViableHead(bestChildNode, currentSlot);
|
|
1025
|
+
if (childLeadsToViableHead && !bestChildLeadsToViableHead) {
|
|
1026
|
+
// the child leads to a viable head, but the current best-child doesn't
|
|
557
1027
|
newChildAndDescendant = changeToChild;
|
|
1028
|
+
break outer;
|
|
558
1029
|
}
|
|
559
|
-
|
|
1030
|
+
if (!childLeadsToViableHead && bestChildLeadsToViableHead) {
|
|
1031
|
+
// the best child leads to a viable head but the child doesn't
|
|
560
1032
|
newChildAndDescendant = noChange;
|
|
1033
|
+
break outer;
|
|
561
1034
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
//
|
|
565
|
-
|
|
1035
|
+
// Both nodes lead to viable heads (or both don't), need to pick winner
|
|
1036
|
+
// Pre-fulu we pick whichever has higher weight, tie-breaker by root
|
|
1037
|
+
// Post-fulu we pick whichever has higher weight, then tie-breaker by root, then tie-breaker by `getPayloadStatusTiebreaker`
|
|
1038
|
+
// Gloas: nodes from previous slot (n-1) with EMPTY/FULL variant have weight hardcoded to 0.
|
|
1039
|
+
// https://github.com/ethereum/consensus-specs/blob/69a2582d5d62c914b24894bdb65f4bd5d4e49ae4/specs/gloas/fork-choice.md?plain=1#L442
|
|
1040
|
+
const childEffectiveWeight = !isGloasBlock(childNode) ||
|
|
1041
|
+
childNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1042
|
+
childNode.slot + 1 !== currentSlot
|
|
1043
|
+
? childNode.weight
|
|
1044
|
+
: 0;
|
|
1045
|
+
const bestChildEffectiveWeight = !isGloasBlock(bestChildNode) ||
|
|
1046
|
+
bestChildNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1047
|
+
bestChildNode.slot + 1 !== currentSlot
|
|
1048
|
+
? bestChildNode.weight
|
|
1049
|
+
: 0;
|
|
1050
|
+
if (childEffectiveWeight !== bestChildEffectiveWeight) {
|
|
1051
|
+
// Different effective weights, choose the winner by weight
|
|
1052
|
+
newChildAndDescendant = childEffectiveWeight >= bestChildEffectiveWeight ? changeToChild : noChange;
|
|
1053
|
+
break outer;
|
|
1054
|
+
}
|
|
1055
|
+
if (childNode.blockRoot !== bestChildNode.blockRoot) {
|
|
1056
|
+
// Different blocks, tie-breaker by root
|
|
1057
|
+
newChildAndDescendant = childNode.blockRoot >= bestChildNode.blockRoot ? changeToChild : noChange;
|
|
1058
|
+
break outer;
|
|
1059
|
+
}
|
|
1060
|
+
// Same effective weight and same root — Gloas EMPTY vs FULL from n-1, tie-breaker by payload status
|
|
1061
|
+
// Note: pre-Gloas, each child node of a block has a unique root, so this point should not be reached
|
|
1062
|
+
const childTiebreaker = this.getPayloadStatusTiebreaker(childNode, currentSlot, proposerBoostRoot);
|
|
1063
|
+
const bestChildTiebreaker = this.getPayloadStatusTiebreaker(bestChildNode, currentSlot, proposerBoostRoot);
|
|
1064
|
+
if (childTiebreaker > bestChildTiebreaker) {
|
|
566
1065
|
newChildAndDescendant = changeToChild;
|
|
567
1066
|
}
|
|
1067
|
+
else if (childTiebreaker < bestChildTiebreaker) {
|
|
1068
|
+
newChildAndDescendant = noChange;
|
|
1069
|
+
}
|
|
568
1070
|
else {
|
|
1071
|
+
// Equal in all aspects, noChange
|
|
569
1072
|
newChildAndDescendant = noChange;
|
|
570
1073
|
}
|
|
571
1074
|
}
|
|
572
1075
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1076
|
+
else if (childLeadsToViableHead) {
|
|
1077
|
+
// There is no current best-child and the child is viable.
|
|
1078
|
+
newChildAndDescendant = changeToChild;
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
// There is no current best-child but the child is not viable.
|
|
1082
|
+
newChildAndDescendant = noChange;
|
|
1083
|
+
}
|
|
581
1084
|
}
|
|
582
1085
|
parentNode.bestChild = newChildAndDescendant[0];
|
|
583
1086
|
parentNode.bestDescendant = newChildAndDescendant[1];
|
|
@@ -646,7 +1149,8 @@ export class ProtoArray {
|
|
|
646
1149
|
return true;
|
|
647
1150
|
}
|
|
648
1151
|
const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch);
|
|
649
|
-
|
|
1152
|
+
const ancestorNode = this.getAncestorOrNull(node.blockRoot, finalizedSlot);
|
|
1153
|
+
return this.finalizedEpoch === 0 || (ancestorNode !== null && this.finalizedRoot === ancestorNode.blockRoot);
|
|
650
1154
|
}
|
|
651
1155
|
/**
|
|
652
1156
|
* Same to getAncestor but it may return null instead of throwing error
|
|
@@ -660,47 +1164,103 @@ export class ProtoArray {
|
|
|
660
1164
|
}
|
|
661
1165
|
}
|
|
662
1166
|
/**
|
|
663
|
-
* Returns the
|
|
1167
|
+
* Returns the node identifier of an ancestor of `blockRoot` at the given `slot`.
|
|
664
1168
|
* (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
|
|
665
1169
|
*
|
|
666
1170
|
* NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
|
|
667
1171
|
*
|
|
668
1172
|
* ### Specification
|
|
669
1173
|
*
|
|
670
|
-
*
|
|
1174
|
+
* Modified for Gloas to return node identifier instead of just root:
|
|
1175
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#modified-get_ancestor
|
|
671
1176
|
*
|
|
672
|
-
*
|
|
1177
|
+
* Pre-Gloas: Returns (root, PAYLOAD_STATUS_FULL)
|
|
1178
|
+
* Gloas: Returns (root, payloadStatus) based on actual node state
|
|
673
1179
|
*/
|
|
674
1180
|
getAncestor(blockRoot, ancestorSlot) {
|
|
675
|
-
|
|
676
|
-
|
|
1181
|
+
// Get any variant to check the block (use variants[0])
|
|
1182
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
1183
|
+
if (variantOrArr == null) {
|
|
677
1184
|
throw new ForkChoiceError({
|
|
678
1185
|
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
679
1186
|
root: blockRoot,
|
|
680
1187
|
});
|
|
681
1188
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1189
|
+
const blockIndex = Array.isArray(variantOrArr) ? variantOrArr[0] : variantOrArr;
|
|
1190
|
+
const block = this.nodes[blockIndex];
|
|
1191
|
+
// If block is at or before queried slot, return PENDING variant (or FULL for pre-Gloas)
|
|
1192
|
+
if (block.slot <= ancestorSlot) {
|
|
1193
|
+
// For pre-Gloas: only FULL exists at variants[0]
|
|
1194
|
+
// For Gloas: PENDING is at variants[0]
|
|
1195
|
+
return block;
|
|
1196
|
+
}
|
|
1197
|
+
// Walk backwards through beacon blocks to find ancestor
|
|
1198
|
+
// Start with the parent of the current block
|
|
1199
|
+
let currentBlock = block;
|
|
1200
|
+
const parentVariants = this.indices.get(currentBlock.parentRoot);
|
|
1201
|
+
if (parentVariants == null) {
|
|
1202
|
+
throw new ForkChoiceError({
|
|
1203
|
+
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
1204
|
+
descendantRoot: blockRoot,
|
|
1205
|
+
ancestorSlot,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
let parentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
|
|
1209
|
+
let parentBlock = this.nodes[parentIndex];
|
|
1210
|
+
// Walk backwards while parent.slot > ancestorSlot
|
|
1211
|
+
while (parentBlock.slot > ancestorSlot) {
|
|
1212
|
+
currentBlock = parentBlock;
|
|
1213
|
+
const nextParentVariants = this.indices.get(currentBlock.parentRoot);
|
|
1214
|
+
if (nextParentVariants == null) {
|
|
1215
|
+
throw new ForkChoiceError({
|
|
1216
|
+
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
1217
|
+
descendantRoot: blockRoot,
|
|
1218
|
+
ancestorSlot,
|
|
1219
|
+
});
|
|
689
1220
|
}
|
|
1221
|
+
parentIndex = Array.isArray(nextParentVariants) ? nextParentVariants[0] : nextParentVariants;
|
|
1222
|
+
parentBlock = this.nodes[parentIndex];
|
|
1223
|
+
}
|
|
1224
|
+
// Now parentBlock.slot <= ancestorSlot
|
|
1225
|
+
// Return the parent with the correct payload status based on currentBlock
|
|
1226
|
+
if (!isGloasBlock(currentBlock)) {
|
|
1227
|
+
// Pre-Gloas: return FULL variant (only one that exists)
|
|
1228
|
+
return parentBlock;
|
|
1229
|
+
}
|
|
1230
|
+
// Gloas: determine which parent variant (EMPTY or FULL) based on parent_block_hash
|
|
1231
|
+
const parentPayloadStatus = this.getParentPayloadStatus(currentBlock);
|
|
1232
|
+
const parentVariantIndex = this.getNodeIndexByRootAndStatus(currentBlock.parentRoot, parentPayloadStatus);
|
|
1233
|
+
if (parentVariantIndex === undefined) {
|
|
690
1234
|
throw new ForkChoiceError({
|
|
691
1235
|
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
692
1236
|
descendantRoot: blockRoot,
|
|
693
1237
|
ancestorSlot,
|
|
694
1238
|
});
|
|
695
1239
|
}
|
|
696
|
-
|
|
697
|
-
|
|
1240
|
+
return this.nodes[parentVariantIndex];
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Get the parent node index for traversal
|
|
1244
|
+
* For Gloas blocks: returns the correct EMPTY/FULL variant based on parent payload status
|
|
1245
|
+
* For pre-Gloas blocks: returns the simple parent index
|
|
1246
|
+
* Returns undefined if parent doesn't exist or can't be found
|
|
1247
|
+
*/
|
|
1248
|
+
getParentNodeIndex(node) {
|
|
1249
|
+
if (isGloasBlock(node)) {
|
|
1250
|
+
// Use getParentPayloadStatus for Gloas blocks to get correct EMPTY/FULL variant
|
|
1251
|
+
const parentPayloadStatus = this.getParentPayloadStatus(node);
|
|
1252
|
+
return this.getNodeIndexByRootAndStatus(node.parentRoot, parentPayloadStatus);
|
|
1253
|
+
}
|
|
1254
|
+
// Simple parent traversal for pre-Gloas blocks (includes fork transition)
|
|
1255
|
+
return node.parent;
|
|
698
1256
|
}
|
|
699
1257
|
/**
|
|
700
1258
|
* Iterate from a block root backwards over nodes
|
|
1259
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1260
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
701
1261
|
*/
|
|
702
|
-
*iterateAncestorNodes(blockRoot) {
|
|
703
|
-
const startIndex = this.
|
|
1262
|
+
*iterateAncestorNodes(blockRoot, payloadStatus) {
|
|
1263
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
704
1264
|
if (startIndex === undefined) {
|
|
705
1265
|
return;
|
|
706
1266
|
}
|
|
@@ -714,19 +1274,28 @@ export class ProtoArray {
|
|
|
714
1274
|
yield* this.iterateAncestorNodesFromNode(node);
|
|
715
1275
|
}
|
|
716
1276
|
/**
|
|
717
|
-
* Iterate from a
|
|
1277
|
+
* Iterate from a node backwards over ancestor nodes
|
|
1278
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1279
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
1280
|
+
* Handles fork transition from Gloas to pre-Gloas blocks
|
|
718
1281
|
*/
|
|
719
1282
|
*iterateAncestorNodesFromNode(node) {
|
|
720
1283
|
while (node.parent !== undefined) {
|
|
721
|
-
|
|
1284
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1285
|
+
if (parentIndex === undefined) {
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
node = this.nodes[parentIndex];
|
|
722
1289
|
yield node;
|
|
723
1290
|
}
|
|
724
1291
|
}
|
|
725
1292
|
/**
|
|
726
1293
|
* Get all nodes from a block root backwards
|
|
1294
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1295
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
727
1296
|
*/
|
|
728
|
-
getAllAncestorNodes(blockRoot) {
|
|
729
|
-
const startIndex = this.
|
|
1297
|
+
getAllAncestorNodes(blockRoot, payloadStatus) {
|
|
1298
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
730
1299
|
if (startIndex === undefined) {
|
|
731
1300
|
return [];
|
|
732
1301
|
}
|
|
@@ -737,9 +1306,17 @@ export class ProtoArray {
|
|
|
737
1306
|
index: startIndex,
|
|
738
1307
|
});
|
|
739
1308
|
}
|
|
740
|
-
|
|
1309
|
+
// Exclude PENDING variant from returned ancestors.
|
|
1310
|
+
const nodes = [];
|
|
1311
|
+
if (node.payloadStatus !== PayloadStatus.PENDING) {
|
|
1312
|
+
nodes.push(node);
|
|
1313
|
+
}
|
|
741
1314
|
while (node.parent !== undefined) {
|
|
742
|
-
|
|
1315
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1316
|
+
if (parentIndex === undefined) {
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
node = this.nodes[parentIndex];
|
|
743
1320
|
nodes.push(node);
|
|
744
1321
|
}
|
|
745
1322
|
return nodes;
|
|
@@ -748,9 +1325,12 @@ export class ProtoArray {
|
|
|
748
1325
|
* The opposite of iterateNodes.
|
|
749
1326
|
* iterateNodes is to find ancestor nodes of a blockRoot.
|
|
750
1327
|
* this is to find non-ancestor nodes of a blockRoot.
|
|
1328
|
+
*
|
|
1329
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1330
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
751
1331
|
*/
|
|
752
|
-
getAllNonAncestorNodes(blockRoot) {
|
|
753
|
-
const startIndex = this.
|
|
1332
|
+
getAllNonAncestorNodes(blockRoot, payloadStatus) {
|
|
1333
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
754
1334
|
if (startIndex === undefined) {
|
|
755
1335
|
return [];
|
|
756
1336
|
}
|
|
@@ -761,23 +1341,31 @@ export class ProtoArray {
|
|
|
761
1341
|
index: startIndex,
|
|
762
1342
|
});
|
|
763
1343
|
}
|
|
1344
|
+
// For both Gloas and pre-Gloas blocks
|
|
764
1345
|
const result = [];
|
|
765
1346
|
let nodeIndex = startIndex;
|
|
766
1347
|
while (node.parent !== undefined) {
|
|
767
|
-
const parentIndex = node
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1348
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1349
|
+
if (parentIndex === undefined) {
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
node = this.nodes[parentIndex];
|
|
1353
|
+
// Collect non-ancestor nodes between current and parent
|
|
1354
|
+
// Filter to exclude PENDING nodes (FULL variant pre-gloas, EMPTY or FULL variant post-gloas)
|
|
1355
|
+
result.push(...this.getNodesBetween(nodeIndex, parentIndex).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
771
1356
|
nodeIndex = parentIndex;
|
|
772
1357
|
}
|
|
773
|
-
|
|
1358
|
+
// Collect remaining nodes from nodeIndex to beginning
|
|
1359
|
+
result.push(...this.getNodesBetween(nodeIndex, 0).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
774
1360
|
return result;
|
|
775
1361
|
}
|
|
776
1362
|
/**
|
|
777
1363
|
* Returns both ancestor and non-ancestor nodes in a single traversal.
|
|
1364
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1365
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
778
1366
|
*/
|
|
779
|
-
getAllAncestorAndNonAncestorNodes(blockRoot) {
|
|
780
|
-
const startIndex = this.
|
|
1367
|
+
getAllAncestorAndNonAncestorNodes(blockRoot, payloadStatus) {
|
|
1368
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
781
1369
|
if (startIndex === undefined) {
|
|
782
1370
|
return { ancestors: [], nonAncestors: [] };
|
|
783
1371
|
}
|
|
@@ -790,32 +1378,63 @@ export class ProtoArray {
|
|
|
790
1378
|
}
|
|
791
1379
|
const ancestors = [];
|
|
792
1380
|
const nonAncestors = [];
|
|
1381
|
+
// Include starting node if it's not PENDING (i.e., pre-Gloas or EMPTY/FULL variant post-Gloas)
|
|
1382
|
+
if (node.payloadStatus !== PayloadStatus.PENDING) {
|
|
1383
|
+
ancestors.push(node);
|
|
1384
|
+
}
|
|
793
1385
|
let nodeIndex = startIndex;
|
|
794
1386
|
while (node.parent !== undefined) {
|
|
1387
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1388
|
+
if (parentIndex === undefined) {
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
node = this.nodes[parentIndex];
|
|
795
1392
|
ancestors.push(node);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
nonAncestors.push(...this.getNodesBetween(nodeIndex, parentIndex));
|
|
1393
|
+
// Collect non-ancestor nodes between current and parent
|
|
1394
|
+
// Filter to exclude PENDING nodes (include all FULL/EMPTY for both pre-Gloas and Gloas)
|
|
1395
|
+
nonAncestors.push(...this.getNodesBetween(nodeIndex, parentIndex).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
800
1396
|
nodeIndex = parentIndex;
|
|
801
1397
|
}
|
|
802
|
-
|
|
803
|
-
nonAncestors.push(...this.getNodesBetween(nodeIndex, 0));
|
|
1398
|
+
// Collect remaining non-ancestor nodes from nodeIndex to beginning
|
|
1399
|
+
nonAncestors.push(...this.getNodesBetween(nodeIndex, 0).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
804
1400
|
return { ancestors, nonAncestors };
|
|
805
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Check if a block exists in the proto array
|
|
1404
|
+
* Uses default variant (PENDING for Gloas, FULL for pre-Gloas)
|
|
1405
|
+
*/
|
|
806
1406
|
hasBlock(blockRoot) {
|
|
807
|
-
return this.
|
|
1407
|
+
return this.getDefaultNodeIndex(blockRoot) !== undefined;
|
|
808
1408
|
}
|
|
809
|
-
|
|
810
|
-
|
|
1409
|
+
/**
|
|
1410
|
+
* Return ProtoNode for blockRoot with explicit payload status
|
|
1411
|
+
*
|
|
1412
|
+
* @param blockRoot - The block root to look up
|
|
1413
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1414
|
+
* @returns The ProtoNode for the specified variant, or undefined if not found
|
|
1415
|
+
*
|
|
1416
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1417
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1418
|
+
*/
|
|
1419
|
+
getNode(blockRoot, payloadStatus) {
|
|
1420
|
+
const blockIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
811
1421
|
if (blockIndex === undefined) {
|
|
812
1422
|
return undefined;
|
|
813
1423
|
}
|
|
814
1424
|
return this.getNodeByIndex(blockIndex);
|
|
815
1425
|
}
|
|
816
|
-
/**
|
|
817
|
-
|
|
818
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1428
|
+
*
|
|
1429
|
+
* @param blockRoot - The block root to look up
|
|
1430
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1431
|
+
* @returns The ProtoBlock for the specified variant (spreads properties), or undefined if not found
|
|
1432
|
+
*
|
|
1433
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1434
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1435
|
+
*/
|
|
1436
|
+
getBlock(blockRoot, payloadStatus) {
|
|
1437
|
+
const node = this.getNode(blockRoot, payloadStatus);
|
|
819
1438
|
if (!node) {
|
|
820
1439
|
return undefined;
|
|
821
1440
|
}
|
|
@@ -823,9 +1442,19 @@ export class ProtoArray {
|
|
|
823
1442
|
...node,
|
|
824
1443
|
};
|
|
825
1444
|
}
|
|
826
|
-
/**
|
|
827
|
-
|
|
828
|
-
|
|
1445
|
+
/**
|
|
1446
|
+
* Return NON-MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1447
|
+
*
|
|
1448
|
+
* @param blockRoot - The block root to look up
|
|
1449
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1450
|
+
* @returns The ProtoBlock for the specified variant (does not spread properties)
|
|
1451
|
+
* @throws Error if block not found
|
|
1452
|
+
*
|
|
1453
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1454
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1455
|
+
*/
|
|
1456
|
+
getBlockReadonly(blockRoot, payloadStatus) {
|
|
1457
|
+
const node = this.getNode(blockRoot, payloadStatus);
|
|
829
1458
|
if (!node) {
|
|
830
1459
|
throw Error(`No block for root ${blockRoot}`);
|
|
831
1460
|
}
|
|
@@ -834,21 +1463,21 @@ export class ProtoArray {
|
|
|
834
1463
|
/**
|
|
835
1464
|
* Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
836
1465
|
* Always returns `false` if either input roots are unknown.
|
|
837
|
-
* Still returns `true` if `ancestorRoot` === `descendantRoot`
|
|
1466
|
+
* Still returns `true` if `ancestorRoot` === `descendantRoot` and payload statuses match.
|
|
838
1467
|
*/
|
|
839
|
-
isDescendant(ancestorRoot, descendantRoot) {
|
|
840
|
-
const ancestorNode = this.getNode(ancestorRoot);
|
|
1468
|
+
isDescendant(ancestorRoot, ancestorPayloadStatus, descendantRoot, descendantPayloadStatus) {
|
|
1469
|
+
const ancestorNode = this.getNode(ancestorRoot, ancestorPayloadStatus);
|
|
841
1470
|
if (!ancestorNode) {
|
|
842
1471
|
return false;
|
|
843
1472
|
}
|
|
844
|
-
if (ancestorRoot === descendantRoot) {
|
|
1473
|
+
if (ancestorRoot === descendantRoot && ancestorPayloadStatus === descendantPayloadStatus) {
|
|
845
1474
|
return true;
|
|
846
1475
|
}
|
|
847
|
-
for (const node of this.iterateAncestorNodes(descendantRoot)) {
|
|
1476
|
+
for (const node of this.iterateAncestorNodes(descendantRoot, descendantPayloadStatus)) {
|
|
848
1477
|
if (node.slot < ancestorNode.slot) {
|
|
849
1478
|
return false;
|
|
850
1479
|
}
|
|
851
|
-
if (node.blockRoot === ancestorNode.blockRoot) {
|
|
1480
|
+
if (node.blockRoot === ancestorNode.blockRoot && node.payloadStatus === ancestorNode.payloadStatus) {
|
|
852
1481
|
return true;
|
|
853
1482
|
}
|
|
854
1483
|
}
|