@lodestar/fork-choice 1.41.0-dev.2074a31ba7 → 1.41.0-dev.21d4a81d4e
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 +764 -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 +897 -135
|
@@ -1,16 +1,41 @@
|
|
|
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
4
|
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
4
|
-
import {toRootHex} from "@lodestar/utils";
|
|
5
|
+
import {bitCount, toRootHex} from "@lodestar/utils";
|
|
5
6
|
import {ForkChoiceError, ForkChoiceErrorCode} from "../forkChoice/errors.js";
|
|
6
7
|
import {LVHExecError, LVHExecErrorCode, ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ExecutionStatus,
|
|
10
|
+
HEX_ZERO_HASH,
|
|
11
|
+
LVHExecResponse,
|
|
12
|
+
PayloadStatus,
|
|
13
|
+
ProtoBlock,
|
|
14
|
+
ProtoNode,
|
|
15
|
+
isGloasBlock,
|
|
16
|
+
} from "./interface.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Threshold for payload timeliness (>50% of PTC must vote)
|
|
20
|
+
* Spec: gloas/fork-choice.md (PAYLOAD_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
21
|
+
*/
|
|
22
|
+
const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
8
23
|
|
|
9
24
|
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
10
25
|
type ProposerBoost = {root: RootHex; score: number};
|
|
11
26
|
|
|
12
27
|
const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));
|
|
13
28
|
|
|
29
|
+
/** Pre-Gloas: single element, FULL index (for backward compatibility) */
|
|
30
|
+
type PreGloasVariantIndex = number;
|
|
31
|
+
/**
|
|
32
|
+
* Post-Gloas: array length is 2 or 3
|
|
33
|
+
* - Length 2: [PENDING_INDEX, EMPTY_INDEX] when payload hasn't arrived yet
|
|
34
|
+
* - Length 3: [PENDING_INDEX, EMPTY_INDEX, FULL_INDEX] when payload has arrived
|
|
35
|
+
*/
|
|
36
|
+
type GloasVariantIndices = [number, number] | [number, number, number];
|
|
37
|
+
type VariantIndices = PreGloasVariantIndex | GloasVariantIndices;
|
|
38
|
+
|
|
14
39
|
export class ProtoArray {
|
|
15
40
|
// Do not attempt to prune the tree unless it has at least this many nodes.
|
|
16
41
|
// Small prunes simply waste time
|
|
@@ -20,11 +45,31 @@ export class ProtoArray {
|
|
|
20
45
|
finalizedEpoch: Epoch;
|
|
21
46
|
finalizedRoot: RootHex;
|
|
22
47
|
nodes: ProtoNode[] = [];
|
|
23
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Maps block root to array of node indices for each payload status variant
|
|
50
|
+
*
|
|
51
|
+
* Array structure: [PENDING, EMPTY, FULL] where indices correspond to PayloadStatus enum values
|
|
52
|
+
* - number[0] = PENDING variant index (PayloadStatus.PENDING = 0)
|
|
53
|
+
* - number[1] = EMPTY variant index (PayloadStatus.EMPTY = 1)
|
|
54
|
+
* - number[2] = FULL variant index (PayloadStatus.FULL = 2)
|
|
55
|
+
*
|
|
56
|
+
* Note: undefined array elements indicate that variant doesn't exist for this block
|
|
57
|
+
*/
|
|
58
|
+
indices = new Map<RootHex, VariantIndices>();
|
|
24
59
|
lvhError?: LVHExecError;
|
|
25
60
|
|
|
26
61
|
private previousProposerBoost: ProposerBoost | null = null;
|
|
27
62
|
|
|
63
|
+
/**
|
|
64
|
+
* PTC (Payload Timeliness Committee) votes per block as bitvectors
|
|
65
|
+
* Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal)
|
|
66
|
+
* Spec: gloas/fork-choice.md#modified-store (line 148)
|
|
67
|
+
*
|
|
68
|
+
* Bit i is set if PTC member i voted payload_present=true
|
|
69
|
+
* Used by is_payload_timely() to determine if payload is timely
|
|
70
|
+
*/
|
|
71
|
+
private ptcVotes = new Map<RootHex, BitArray>();
|
|
72
|
+
|
|
28
73
|
constructor({
|
|
29
74
|
pruneThreshold,
|
|
30
75
|
justifiedEpoch,
|
|
@@ -59,11 +104,186 @@ export class ProtoArray {
|
|
|
59
104
|
// We are using the blockROot as the targetRoot, since it always lies on an epoch boundary
|
|
60
105
|
targetRoot: block.blockRoot,
|
|
61
106
|
} as ProtoBlock,
|
|
62
|
-
currentSlot
|
|
107
|
+
currentSlot,
|
|
108
|
+
null
|
|
63
109
|
);
|
|
64
110
|
return protoArray;
|
|
65
111
|
}
|
|
66
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Get node index for a block root and payload status
|
|
115
|
+
*
|
|
116
|
+
* @param root - The block root to look up
|
|
117
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
118
|
+
* @returns The node index for the specified variant, or undefined if not found
|
|
119
|
+
*
|
|
120
|
+
* Behavior:
|
|
121
|
+
* - Pre-Gloas blocks: only FULL is valid, PENDING/EMPTY throw error
|
|
122
|
+
* - Gloas blocks: returns the specified variant index, or undefined if that variant doesn't exist
|
|
123
|
+
*
|
|
124
|
+
* Note: payloadStatus is required. Use getDefaultVariant() to get the canonical variant.
|
|
125
|
+
*/
|
|
126
|
+
getNodeIndexByRootAndStatus(root: RootHex, payloadStatus: PayloadStatus): number | undefined {
|
|
127
|
+
const variantOrArr = this.indices.get(root);
|
|
128
|
+
if (variantOrArr == null) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Pre-Gloas: only FULL variant exists
|
|
133
|
+
if (!Array.isArray(variantOrArr)) {
|
|
134
|
+
// Return FULL variant if no status specified or FULL explicitly requested
|
|
135
|
+
if (payloadStatus === PayloadStatus.FULL) {
|
|
136
|
+
return variantOrArr;
|
|
137
|
+
}
|
|
138
|
+
// PENDING and EMPTY are invalid for pre-Gloas blocks
|
|
139
|
+
throw new ProtoArrayError({
|
|
140
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
141
|
+
index: payloadStatus,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Gloas: return the specified variant, or PENDING if not specified
|
|
146
|
+
return variantOrArr[payloadStatus];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the default/canonical payload status for a block root
|
|
151
|
+
* - Pre-Gloas blocks: Returns FULL (payload embedded in block)
|
|
152
|
+
* - Gloas blocks: Returns PENDING (canonical variant)
|
|
153
|
+
*
|
|
154
|
+
* @param blockRoot - The block root to check
|
|
155
|
+
* @returns PayloadStatus.FULL for pre-Gloas, PayloadStatus.PENDING for Gloas, undefined if block not found
|
|
156
|
+
*/
|
|
157
|
+
getDefaultVariant(blockRoot: RootHex): PayloadStatus | undefined {
|
|
158
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
159
|
+
if (variantOrArr == null) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pre-Gloas: only FULL variant exists
|
|
164
|
+
if (!Array.isArray(variantOrArr)) {
|
|
165
|
+
return PayloadStatus.FULL;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Gloas: multiple variants exist, PENDING is canonical
|
|
169
|
+
return PayloadStatus.PENDING;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the node index for the default/canonical variant in a single hash lookup.
|
|
174
|
+
* - Pre-Gloas blocks: returns the FULL variant index
|
|
175
|
+
* - Gloas blocks: returns the PENDING variant index
|
|
176
|
+
*/
|
|
177
|
+
getDefaultNodeIndex(blockRoot: RootHex): number | undefined {
|
|
178
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
179
|
+
if (variantOrArr == null) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Pre-Gloas: value is the index directly
|
|
184
|
+
if (!Array.isArray(variantOrArr)) {
|
|
185
|
+
return variantOrArr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Gloas: PENDING is the canonical variant
|
|
189
|
+
return variantOrArr[PayloadStatus.PENDING];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Determine which parent payload status a block extends
|
|
194
|
+
* Spec: gloas/fork-choice.md#new-get_parent_payload_status
|
|
195
|
+
* def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
|
|
196
|
+
* parent = store.blocks[block.parent_root]
|
|
197
|
+
* parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
|
|
198
|
+
* message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
|
|
199
|
+
* return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY
|
|
200
|
+
*
|
|
201
|
+
* In lodestar forkchoice, we don't store the full bid, so we compares parent_block_hash in child's bid with executionPayloadBlockHash in parent:
|
|
202
|
+
* - If it matches EMPTY variant, return EMPTY
|
|
203
|
+
* - If it matches FULL variant, return FULL
|
|
204
|
+
* - If no match, throw UNKNOWN_PARENT_BLOCK error
|
|
205
|
+
*
|
|
206
|
+
* For pre-Gloas blocks: always returns FULL
|
|
207
|
+
*/
|
|
208
|
+
getParentPayloadStatus(block: ProtoBlock): PayloadStatus {
|
|
209
|
+
// Pre-Gloas blocks have payloads embedded, so parents are always FULL
|
|
210
|
+
const {parentBlockHash} = block;
|
|
211
|
+
if (parentBlockHash === null) {
|
|
212
|
+
return PayloadStatus.FULL;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const parentBlock = this.getBlockHexAndBlockHash(block.parentRoot, parentBlockHash);
|
|
216
|
+
if (parentBlock == null) {
|
|
217
|
+
throw new ProtoArrayError({
|
|
218
|
+
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
|
|
219
|
+
parentRoot: block.parentRoot,
|
|
220
|
+
parentHash: parentBlockHash,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return parentBlock.payloadStatus;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Return the parent `ProtoBlock` given its root and block hash.
|
|
229
|
+
*/
|
|
230
|
+
getParent(parentRoot: RootHex, parentBlockHash: RootHex | null): ProtoBlock | null {
|
|
231
|
+
// pre-gloas
|
|
232
|
+
if (parentBlockHash === null) {
|
|
233
|
+
const parentIndex = this.indices.get(parentRoot);
|
|
234
|
+
if (parentIndex === undefined) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(parentIndex)) {
|
|
238
|
+
// Gloas block found when pre-gloas expected
|
|
239
|
+
throw new ProtoArrayError({
|
|
240
|
+
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
|
|
241
|
+
parentRoot,
|
|
242
|
+
parentHash: parentBlockHash,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return this.nodes[parentIndex] ?? null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// post-gloas
|
|
249
|
+
return this.getBlockHexAndBlockHash(parentRoot, parentBlockHash);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
|
|
254
|
+
*/
|
|
255
|
+
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
|
|
256
|
+
const variantIndices = this.indices.get(blockRoot);
|
|
257
|
+
if (variantIndices === undefined) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Pre-Gloas
|
|
262
|
+
if (!Array.isArray(variantIndices)) {
|
|
263
|
+
const node = this.nodes[variantIndices];
|
|
264
|
+
return node.executionPayloadBlockHash === blockHash ? node : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Post-Gloas, check empty and full variants
|
|
268
|
+
const fullNodeIndex = variantIndices[PayloadStatus.FULL];
|
|
269
|
+
if (fullNodeIndex !== undefined) {
|
|
270
|
+
const fullNode = this.nodes[fullNodeIndex];
|
|
271
|
+
if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
|
|
272
|
+
return fullNode;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
|
|
277
|
+
if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
|
|
278
|
+
return emptyNode;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// PENDING is the same to EMPTY so not likely we can return it
|
|
282
|
+
// also it's only specific for fork-choice
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
67
287
|
/**
|
|
68
288
|
* Iterate backwards through the array, touching all nodes and their parents and potentially
|
|
69
289
|
* the best-child of each parent.
|
|
@@ -96,11 +316,11 @@ export class ProtoArray {
|
|
|
96
316
|
finalizedRoot: RootHex;
|
|
97
317
|
currentSlot: Slot;
|
|
98
318
|
}): void {
|
|
99
|
-
if (deltas.length !== this.
|
|
319
|
+
if (deltas.length !== this.nodes.length) {
|
|
100
320
|
throw new ProtoArrayError({
|
|
101
321
|
code: ProtoArrayErrorCode.INVALID_DELTA_LEN,
|
|
102
322
|
deltas: deltas.length,
|
|
103
|
-
indices: this.
|
|
323
|
+
indices: this.nodes.length,
|
|
104
324
|
});
|
|
105
325
|
}
|
|
106
326
|
|
|
@@ -171,6 +391,7 @@ export class ProtoArray {
|
|
|
171
391
|
// We _must_ perform these functions separate from the weight-updating loop above to ensure
|
|
172
392
|
// that we have a fully coherent set of weights before updating parent
|
|
173
393
|
// best-child/descendant.
|
|
394
|
+
const proposerBoostRoot = proposerBoost?.root ?? null;
|
|
174
395
|
for (let nodeIndex = this.nodes.length - 1; nodeIndex >= 0; nodeIndex--) {
|
|
175
396
|
const node = this.nodes[nodeIndex];
|
|
176
397
|
if (node === undefined) {
|
|
@@ -183,7 +404,7 @@ export class ProtoArray {
|
|
|
183
404
|
// If the node has a parent, try to update its best-child and best-descendant.
|
|
184
405
|
const parentIndex = node.parent;
|
|
185
406
|
if (parentIndex !== undefined) {
|
|
186
|
-
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot);
|
|
407
|
+
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot, proposerBoostRoot);
|
|
187
408
|
}
|
|
188
409
|
}
|
|
189
410
|
// Update the previous proposer boost
|
|
@@ -195,9 +416,9 @@ export class ProtoArray {
|
|
|
195
416
|
*
|
|
196
417
|
* It is only sane to supply an undefined parent for the genesis block
|
|
197
418
|
*/
|
|
198
|
-
onBlock(block: ProtoBlock, currentSlot: Slot): void {
|
|
419
|
+
onBlock(block: ProtoBlock, currentSlot: Slot, proposerBoostRoot: RootHex | null): void {
|
|
199
420
|
// If the block is already known, simply ignore it
|
|
200
|
-
if (this.
|
|
421
|
+
if (this.hasBlock(block.blockRoot)) {
|
|
201
422
|
return;
|
|
202
423
|
}
|
|
203
424
|
if (block.executionStatus === ExecutionStatus.Invalid) {
|
|
@@ -207,35 +428,301 @@ export class ProtoArray {
|
|
|
207
428
|
});
|
|
208
429
|
}
|
|
209
430
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parent
|
|
431
|
+
if (isGloasBlock(block)) {
|
|
432
|
+
// Gloas: Create PENDING + EMPTY nodes with correct parent relationships
|
|
433
|
+
// Parent of new PENDING node = parent block's EMPTY or FULL (inter-block edge)
|
|
434
|
+
// Parent of new EMPTY node = own PENDING node (intra-block edge)
|
|
435
|
+
|
|
436
|
+
// For fork transition: if parent is pre-Gloas, point to parent's FULL
|
|
437
|
+
// Otherwise, determine which parent payload status this block extends
|
|
438
|
+
let parentIndex: number | undefined;
|
|
439
|
+
|
|
440
|
+
// Check if parent exists by getting variants array
|
|
441
|
+
const parentVariants = this.indices.get(block.parentRoot);
|
|
442
|
+
if (parentVariants != null) {
|
|
443
|
+
const anyParentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
|
|
444
|
+
const anyParentNode = this.nodes[anyParentIndex];
|
|
445
|
+
|
|
446
|
+
if (!isGloasBlock(anyParentNode)) {
|
|
447
|
+
// Fork transition: parent is pre-Gloas, so it only has FULL variant at variants[0]
|
|
448
|
+
parentIndex = anyParentIndex;
|
|
449
|
+
} else {
|
|
450
|
+
// Both blocks are Gloas: determine which parent payload status to extend
|
|
451
|
+
const parentPayloadStatus = this.getParentPayloadStatus(block);
|
|
452
|
+
parentIndex = this.getNodeIndexByRootAndStatus(block.parentRoot, parentPayloadStatus);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// else: parent doesn't exist, parentIndex remains undefined (orphan block)
|
|
456
|
+
|
|
457
|
+
// Create PENDING node
|
|
458
|
+
const pendingNode: ProtoNode = {
|
|
459
|
+
...block,
|
|
460
|
+
parent: parentIndex, // Points to parent's EMPTY/FULL or FULL (for transition)
|
|
461
|
+
payloadStatus: PayloadStatus.PENDING,
|
|
462
|
+
weight: 0,
|
|
463
|
+
bestChild: undefined,
|
|
464
|
+
bestDescendant: undefined,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const pendingIndex = this.nodes.length;
|
|
468
|
+
this.nodes.push(pendingNode);
|
|
469
|
+
|
|
470
|
+
// Create EMPTY variant as a child of PENDING
|
|
471
|
+
const emptyNode: ProtoNode = {
|
|
472
|
+
...block,
|
|
473
|
+
parent: pendingIndex, // Points to own PENDING
|
|
474
|
+
payloadStatus: PayloadStatus.EMPTY,
|
|
475
|
+
weight: 0,
|
|
476
|
+
bestChild: undefined,
|
|
477
|
+
bestDescendant: undefined,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const emptyIndex = this.nodes.length;
|
|
481
|
+
this.nodes.push(emptyNode);
|
|
482
|
+
|
|
483
|
+
// Store both variants in the indices array
|
|
484
|
+
// [PENDING, EMPTY, undefined] - FULL will be added later if payload arrives
|
|
485
|
+
this.indices.set(block.blockRoot, [pendingIndex, emptyIndex]);
|
|
486
|
+
|
|
487
|
+
// Update bestChild pointers
|
|
488
|
+
if (parentIndex !== undefined) {
|
|
489
|
+
this.maybeUpdateBestChildAndDescendant(parentIndex, pendingIndex, currentSlot, proposerBoostRoot);
|
|
490
|
+
|
|
491
|
+
if (pendingNode.executionStatus === ExecutionStatus.Valid) {
|
|
492
|
+
this.propagateValidExecutionStatusByIndex(parentIndex);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Update bestChild for PENDING → EMPTY edge
|
|
497
|
+
this.maybeUpdateBestChildAndDescendant(pendingIndex, emptyIndex, currentSlot, proposerBoostRoot);
|
|
498
|
+
|
|
499
|
+
// Initialize PTC votes for this block (all false initially)
|
|
500
|
+
// Spec: gloas/fork-choice.md#modified-on_block (line 645)
|
|
501
|
+
this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
502
|
+
} else {
|
|
503
|
+
// Pre-Gloas: Only create FULL node (payload embedded in block)
|
|
504
|
+
const node: ProtoNode = {
|
|
505
|
+
...block,
|
|
506
|
+
parent: this.getNodeIndexByRootAndStatus(block.parentRoot, PayloadStatus.FULL),
|
|
507
|
+
payloadStatus: PayloadStatus.FULL,
|
|
508
|
+
weight: 0,
|
|
509
|
+
bestChild: undefined,
|
|
510
|
+
bestDescendant: undefined,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const nodeIndex = this.nodes.length;
|
|
514
|
+
this.nodes.push(node);
|
|
515
|
+
|
|
516
|
+
// Pre-Gloas: store FULL index instead of array
|
|
517
|
+
this.indices.set(block.blockRoot, nodeIndex);
|
|
518
|
+
|
|
519
|
+
// If this node is valid, lets propagate the valid status up the chain
|
|
520
|
+
// and throw error if we counter invalid, as this breaks consensus
|
|
521
|
+
if (node.parent !== undefined) {
|
|
522
|
+
this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot, proposerBoostRoot);
|
|
523
|
+
|
|
524
|
+
if (node.executionStatus === ExecutionStatus.Valid) {
|
|
525
|
+
this.propagateValidExecutionStatusByIndex(node.parent);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Called when an execution payload is received for a block (Gloas only)
|
|
533
|
+
* Creates a FULL variant node as a sibling to the existing EMPTY variant
|
|
534
|
+
* Both EMPTY and FULL have parent = own PENDING node
|
|
535
|
+
*
|
|
536
|
+
* Spec: gloas/fork-choice.md (on_execution_payload event)
|
|
537
|
+
*/
|
|
538
|
+
onExecutionPayload(
|
|
539
|
+
blockRoot: RootHex,
|
|
540
|
+
currentSlot: Slot,
|
|
541
|
+
executionPayloadBlockHash: RootHex,
|
|
542
|
+
executionPayloadNumber: number,
|
|
543
|
+
executionPayloadStateRoot: RootHex,
|
|
544
|
+
proposerBoostRoot: RootHex | null
|
|
545
|
+
): void {
|
|
546
|
+
// First check if block exists
|
|
547
|
+
const variants = this.indices.get(blockRoot);
|
|
548
|
+
if (variants == null) {
|
|
549
|
+
// Equivalent to `assert envelope.beacon_block_root in store.block_states`
|
|
550
|
+
throw new ProtoArrayError({
|
|
551
|
+
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
|
|
552
|
+
root: blockRoot,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!Array.isArray(variants)) {
|
|
557
|
+
// Pre-gloas block should not be calling this method
|
|
558
|
+
throw new ProtoArrayError({
|
|
559
|
+
code: ProtoArrayErrorCode.PRE_GLOAS_BLOCK,
|
|
560
|
+
root: blockRoot,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check if FULL already exists for Gloas blocks
|
|
565
|
+
if (variants[PayloadStatus.FULL] !== undefined) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Get PENDING node for Gloas blocks
|
|
570
|
+
const pendingIndex = variants[PayloadStatus.PENDING];
|
|
571
|
+
if (pendingIndex === undefined) {
|
|
572
|
+
throw new ProtoArrayError({
|
|
573
|
+
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
|
|
574
|
+
root: blockRoot,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const pendingNode = this.nodes[pendingIndex];
|
|
579
|
+
if (!pendingNode) {
|
|
580
|
+
throw new ProtoArrayError({
|
|
581
|
+
code: ProtoArrayErrorCode.INVALID_NODE_INDEX,
|
|
582
|
+
index: pendingIndex,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Create FULL variant as a child of PENDING (sibling to EMPTY)
|
|
587
|
+
const fullNode: ProtoNode = {
|
|
588
|
+
...pendingNode,
|
|
589
|
+
parent: pendingIndex, // Points to own PENDING (same as EMPTY)
|
|
590
|
+
payloadStatus: PayloadStatus.FULL,
|
|
213
591
|
weight: 0,
|
|
214
592
|
bestChild: undefined,
|
|
215
593
|
bestDescendant: undefined,
|
|
594
|
+
executionStatus: ExecutionStatus.Valid,
|
|
595
|
+
executionPayloadBlockHash,
|
|
596
|
+
executionPayloadNumber,
|
|
597
|
+
stateRoot: executionPayloadStateRoot,
|
|
216
598
|
};
|
|
217
599
|
|
|
218
|
-
const
|
|
600
|
+
const fullIndex = this.nodes.length;
|
|
601
|
+
this.nodes.push(fullNode);
|
|
219
602
|
|
|
220
|
-
|
|
221
|
-
|
|
603
|
+
// Add FULL variant to the indices array
|
|
604
|
+
variants[PayloadStatus.FULL] = fullIndex;
|
|
222
605
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot);
|
|
606
|
+
// Update bestChild for PENDING node (may now prefer FULL over EMPTY)
|
|
607
|
+
this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot);
|
|
608
|
+
}
|
|
227
609
|
|
|
228
|
-
|
|
229
|
-
|
|
610
|
+
/**
|
|
611
|
+
* Update PTC votes for multiple validators attesting to a block
|
|
612
|
+
* Spec: gloas/fork-choice.md#new-on_payload_attestation_message
|
|
613
|
+
*
|
|
614
|
+
* @param blockRoot - The beacon block root being attested
|
|
615
|
+
* @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1)
|
|
616
|
+
* @param payloadPresent - Whether the validators attest the payload is present
|
|
617
|
+
*/
|
|
618
|
+
notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void {
|
|
619
|
+
const votes = this.ptcVotes.get(blockRoot);
|
|
620
|
+
if (votes === undefined) {
|
|
621
|
+
// Block not found or not a Gloas block, ignore
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (const ptcIndex of ptcIndices) {
|
|
626
|
+
if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
|
|
627
|
+
throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
|
|
230
628
|
}
|
|
629
|
+
|
|
630
|
+
votes.set(ptcIndex, payloadPresent);
|
|
231
631
|
}
|
|
232
632
|
}
|
|
233
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Check if execution payload for a block is timely
|
|
636
|
+
* Spec: gloas/fork-choice.md#new-is_payload_timely
|
|
637
|
+
*
|
|
638
|
+
* Returns true if:
|
|
639
|
+
* 1. Block has PTC votes tracked
|
|
640
|
+
* 2. Payload is locally available (FULL variant exists in proto array)
|
|
641
|
+
* 3. More than PAYLOAD_TIMELY_THRESHOLD (>50% of PTC) members voted payload_present=true
|
|
642
|
+
*
|
|
643
|
+
* @param blockRoot - The beacon block root to check
|
|
644
|
+
*/
|
|
645
|
+
isPayloadTimely(blockRoot: RootHex): boolean {
|
|
646
|
+
const votes = this.ptcVotes.get(blockRoot);
|
|
647
|
+
if (votes === undefined) {
|
|
648
|
+
// Block not found or not a Gloas block
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// If payload is not locally available, it's not timely
|
|
653
|
+
// In our implementation, payload is locally available if proto array has FULL variant of the block
|
|
654
|
+
const fullNodeIndex = this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL);
|
|
655
|
+
if (fullNodeIndex === undefined) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Count votes for payload_present=true
|
|
660
|
+
const yesVotes = bitCount(votes.uint8Array);
|
|
661
|
+
return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Check if parent node is FULL
|
|
666
|
+
* Spec: gloas/fork-choice.md#new-is_parent_node_full
|
|
667
|
+
*
|
|
668
|
+
* Returns true if the parent payload status (determined by block.parentBlockHash) is FULL
|
|
669
|
+
*/
|
|
670
|
+
isParentNodeFull(block: ProtoBlock): boolean {
|
|
671
|
+
return this.getParentPayloadStatus(block) === PayloadStatus.FULL;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Determine if we should extend the payload (prefer FULL over EMPTY)
|
|
676
|
+
* Spec: gloas/fork-choice.md#new-should_extend_payload
|
|
677
|
+
*
|
|
678
|
+
* Returns true if:
|
|
679
|
+
* 1. Payload is timely, OR
|
|
680
|
+
* 2. No proposer boost root (empty/zero hash), OR
|
|
681
|
+
* 3. Proposer boost root's parent is not this block, OR
|
|
682
|
+
* 4. Proposer boost root extends FULL parent
|
|
683
|
+
*
|
|
684
|
+
* @param blockRoot - The block root to check
|
|
685
|
+
* @param proposerBoostRoot - Current proposer boost root (from ForkChoice)
|
|
686
|
+
*/
|
|
687
|
+
shouldExtendPayload(blockRoot: RootHex, proposerBoostRoot: RootHex | null): boolean {
|
|
688
|
+
// Condition 1: Payload is timely
|
|
689
|
+
if (this.isPayloadTimely(blockRoot)) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Condition 2: No proposer boost root
|
|
694
|
+
if (proposerBoostRoot === null || proposerBoostRoot === HEX_ZERO_HASH) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Get proposer boost block
|
|
699
|
+
// We don't care about variant here, just need proposer boost block info
|
|
700
|
+
const proposerBoostIndex = this.getDefaultNodeIndex(proposerBoostRoot);
|
|
701
|
+
const proposerBoostBlock = proposerBoostIndex !== undefined ? this.getNodeByIndex(proposerBoostIndex) : undefined;
|
|
702
|
+
if (!proposerBoostBlock) {
|
|
703
|
+
// Proposer boost block not found, default to extending payload
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Condition 3: Proposer boost root's parent is not this block
|
|
708
|
+
if (proposerBoostBlock.parentRoot !== blockRoot) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Condition 4: Proposer boost root extends FULL parent
|
|
713
|
+
if (this.isParentNodeFull(proposerBoostBlock)) {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
234
720
|
/**
|
|
235
721
|
* Optimistic sync validate till validated latest hash, invalidate any descendant branch
|
|
236
722
|
* if invalidate till hash provided. If consensus fails, this will invalidate entire
|
|
237
723
|
* forkChoice which will throw on any call to findHead
|
|
238
724
|
*/
|
|
725
|
+
// TODO GLOAS: Review usage of this post-gloas
|
|
239
726
|
validateLatestHash(execResponse: LVHExecResponse, currentSlot: Slot): void {
|
|
240
727
|
// Look reverse because its highly likely node with latestValidExecHash is towards the
|
|
241
728
|
// the leaves of the forkchoice
|
|
@@ -279,7 +766,8 @@ export class ProtoArray {
|
|
|
279
766
|
// if its in fcU.
|
|
280
767
|
//
|
|
281
768
|
const {invalidateFromParentBlockRoot, latestValidExecHash} = execResponse;
|
|
282
|
-
|
|
769
|
+
// TODO GLOAS: verify if getting the default/canonical node index is correct here
|
|
770
|
+
const invalidateFromParentIndex = this.getDefaultNodeIndex(invalidateFromParentBlockRoot);
|
|
283
771
|
if (invalidateFromParentIndex === undefined) {
|
|
284
772
|
throw Error(`Unable to find invalidateFromParentBlockRoot=${invalidateFromParentBlockRoot} in forkChoice`);
|
|
285
773
|
}
|
|
@@ -317,6 +805,12 @@ export class ProtoArray {
|
|
|
317
805
|
if (node.executionStatus === ExecutionStatus.PreMerge || node.executionStatus === ExecutionStatus.Valid) {
|
|
318
806
|
break;
|
|
319
807
|
}
|
|
808
|
+
// If PayloadSeparated, that means the node is either PENDING or EMPTY, there could be
|
|
809
|
+
// some ancestor still has syncing status.
|
|
810
|
+
if (node.executionStatus === ExecutionStatus.PayloadSeparated) {
|
|
811
|
+
nodeIndex = node.parent;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
320
814
|
this.validateNodeByIndex(nodeIndex);
|
|
321
815
|
nodeIndex = node.parent;
|
|
322
816
|
}
|
|
@@ -438,10 +932,46 @@ export class ProtoArray {
|
|
|
438
932
|
return validNode;
|
|
439
933
|
}
|
|
440
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Get payload status tiebreaker for fork choice comparison
|
|
937
|
+
* Spec: gloas/fork-choice.md#new-get_payload_status_tiebreaker
|
|
938
|
+
*
|
|
939
|
+
* For PENDING nodes: always returns 0
|
|
940
|
+
* For EMPTY/FULL variants from slot n-1: implements tiebreaker logic based on should_extend_payload
|
|
941
|
+
* For older blocks: returns node.payloadStatus
|
|
942
|
+
*
|
|
943
|
+
* 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.
|
|
944
|
+
*/
|
|
945
|
+
private getPayloadStatusTiebreaker(node: ProtoNode, currentSlot: Slot, proposerBoostRoot: RootHex | null): number {
|
|
946
|
+
// PENDING nodes always return PENDING (no tiebreaker needed)
|
|
947
|
+
// PENDING=0, EMPTY=1, FULL=2
|
|
948
|
+
if (node.payloadStatus === PayloadStatus.PENDING) {
|
|
949
|
+
return node.payloadStatus;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// For Gloas: check if from previous slot
|
|
953
|
+
if (node.slot + 1 !== currentSlot) {
|
|
954
|
+
return node.payloadStatus;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// For previous slot blocks in Gloas, decide between FULL and EMPTY
|
|
958
|
+
// based on should_extend_payload
|
|
959
|
+
if (node.payloadStatus === PayloadStatus.EMPTY) {
|
|
960
|
+
return PayloadStatus.EMPTY;
|
|
961
|
+
}
|
|
962
|
+
// FULL - check should_extend_payload
|
|
963
|
+
const shouldExtend = this.shouldExtendPayload(node.blockRoot, proposerBoostRoot);
|
|
964
|
+
return shouldExtend ? PayloadStatus.FULL : PayloadStatus.PENDING;
|
|
965
|
+
}
|
|
966
|
+
|
|
441
967
|
/**
|
|
442
968
|
* Follows the best-descendant links to find the best-block (i.e., head-block).
|
|
969
|
+
*
|
|
970
|
+
* Returns the ProtoNode representing the head.
|
|
971
|
+
* For pre-Gloas forks, only FULL variants exist (payload embedded).
|
|
972
|
+
* For Gloas, may return PENDING/EMPTY/FULL variants.
|
|
443
973
|
*/
|
|
444
|
-
findHead(justifiedRoot: RootHex, currentSlot: Slot):
|
|
974
|
+
findHead(justifiedRoot: RootHex, currentSlot: Slot): ProtoNode {
|
|
445
975
|
if (this.lvhError) {
|
|
446
976
|
throw new ProtoArrayError({
|
|
447
977
|
code: ProtoArrayErrorCode.INVALID_LVH_EXECUTION_RESPONSE,
|
|
@@ -449,7 +979,8 @@ export class ProtoArray {
|
|
|
449
979
|
});
|
|
450
980
|
}
|
|
451
981
|
|
|
452
|
-
|
|
982
|
+
// Get canonical node: FULL for pre-Gloas, PENDING for Gloas
|
|
983
|
+
const justifiedIndex = this.getDefaultNodeIndex(justifiedRoot);
|
|
453
984
|
if (justifiedIndex === undefined) {
|
|
454
985
|
throw new ProtoArrayError({
|
|
455
986
|
code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN,
|
|
@@ -501,7 +1032,7 @@ export class ProtoArray {
|
|
|
501
1032
|
});
|
|
502
1033
|
}
|
|
503
1034
|
|
|
504
|
-
return bestNode
|
|
1035
|
+
return bestNode;
|
|
505
1036
|
}
|
|
506
1037
|
|
|
507
1038
|
/**
|
|
@@ -520,26 +1051,40 @@ export class ProtoArray {
|
|
|
520
1051
|
* - There is some internal error relating to invalid indices inside `this`.
|
|
521
1052
|
*/
|
|
522
1053
|
maybePrune(finalizedRoot: RootHex): ProtoBlock[] {
|
|
523
|
-
const
|
|
524
|
-
if (
|
|
1054
|
+
const variants = this.indices.get(finalizedRoot);
|
|
1055
|
+
if (variants == null) {
|
|
525
1056
|
throw new ProtoArrayError({
|
|
526
1057
|
code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN,
|
|
527
1058
|
root: finalizedRoot,
|
|
528
1059
|
});
|
|
529
1060
|
}
|
|
530
1061
|
|
|
1062
|
+
// Find the minimum index among all variants to ensure we don't prune too much
|
|
1063
|
+
const finalizedIndex = Array.isArray(variants)
|
|
1064
|
+
? Math.min(...variants.filter((idx) => idx !== undefined))
|
|
1065
|
+
: variants;
|
|
1066
|
+
|
|
531
1067
|
if (finalizedIndex < this.pruneThreshold) {
|
|
532
1068
|
// Pruning at small numbers incurs more cost than benefit
|
|
533
1069
|
return [];
|
|
534
1070
|
}
|
|
535
1071
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
1072
|
+
// Collect all block roots that will be pruned
|
|
1073
|
+
const prunedRoots = new Set<RootHex>();
|
|
1074
|
+
for (let i = 0; i < finalizedIndex; i++) {
|
|
1075
|
+
const node = this.nodes[i];
|
|
539
1076
|
if (node === undefined) {
|
|
540
|
-
throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index:
|
|
1077
|
+
throw new ProtoArrayError({code: ProtoArrayErrorCode.INVALID_NODE_INDEX, index: i});
|
|
541
1078
|
}
|
|
542
|
-
|
|
1079
|
+
prunedRoots.add(node.blockRoot);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Remove indices for pruned blocks and PTC votes
|
|
1083
|
+
for (const root of prunedRoots) {
|
|
1084
|
+
this.indices.delete(root);
|
|
1085
|
+
// Prune PTC votes for this block to prevent memory leak
|
|
1086
|
+
// Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
|
|
1087
|
+
this.ptcVotes.delete(root);
|
|
543
1088
|
}
|
|
544
1089
|
|
|
545
1090
|
// Store nodes prior to finalization
|
|
@@ -547,15 +1092,35 @@ export class ProtoArray {
|
|
|
547
1092
|
// Drop all the nodes prior to finalization
|
|
548
1093
|
this.nodes = this.nodes.slice(finalizedIndex);
|
|
549
1094
|
|
|
550
|
-
// Adjust the indices map
|
|
551
|
-
for (const [
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1095
|
+
// Adjust the indices map - subtract finalizedIndex from all node indices
|
|
1096
|
+
for (const [root, variantIndices] of this.indices.entries()) {
|
|
1097
|
+
// Pre-Gloas: single index
|
|
1098
|
+
if (!Array.isArray(variantIndices)) {
|
|
1099
|
+
if (variantIndices < finalizedIndex) {
|
|
1100
|
+
throw new ProtoArrayError({
|
|
1101
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
1102
|
+
value: "indices",
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
this.indices.set(root, variantIndices - finalizedIndex);
|
|
1106
|
+
continue;
|
|
557
1107
|
}
|
|
558
|
-
|
|
1108
|
+
|
|
1109
|
+
// Post-Gloas: array of variant indices
|
|
1110
|
+
const adjustedVariants = variantIndices.map((variantIndex) => {
|
|
1111
|
+
if (variantIndex === undefined) {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (variantIndex < finalizedIndex) {
|
|
1116
|
+
throw new ProtoArrayError({
|
|
1117
|
+
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
|
|
1118
|
+
value: "indices",
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return variantIndex - finalizedIndex;
|
|
1122
|
+
});
|
|
1123
|
+
this.indices.set(root, adjustedVariants as GloasVariantIndices);
|
|
559
1124
|
}
|
|
560
1125
|
|
|
561
1126
|
// Iterate through all the existing nodes and adjust their indices to match the new layout of this.nodes
|
|
@@ -604,7 +1169,13 @@ export class ProtoArray {
|
|
|
604
1169
|
* - The child is not the best child but becomes the best child.
|
|
605
1170
|
* - The child is not the best child and does not become the best child.
|
|
606
1171
|
*/
|
|
607
|
-
|
|
1172
|
+
|
|
1173
|
+
maybeUpdateBestChildAndDescendant(
|
|
1174
|
+
parentIndex: number,
|
|
1175
|
+
childIndex: number,
|
|
1176
|
+
currentSlot: Slot,
|
|
1177
|
+
proposerBoostRoot: RootHex | null
|
|
1178
|
+
): void {
|
|
608
1179
|
const childNode = this.nodes[childIndex];
|
|
609
1180
|
if (childNode === undefined) {
|
|
610
1181
|
throw new ProtoArrayError({
|
|
@@ -634,54 +1205,90 @@ export class ProtoArray {
|
|
|
634
1205
|
|
|
635
1206
|
let newChildAndDescendant: ChildAndDescendant;
|
|
636
1207
|
const bestChildIndex = parentNode.bestChild;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1208
|
+
// biome-ignore lint/suspicious/noConfusingLabels: labeled block used for early exit from complex decision tree
|
|
1209
|
+
outer: {
|
|
1210
|
+
if (bestChildIndex !== undefined) {
|
|
1211
|
+
if (bestChildIndex === childIndex && !childLeadsToViableHead) {
|
|
1212
|
+
// the child is already the best-child of the parent but its not viable for the head
|
|
1213
|
+
// so remove it
|
|
1214
|
+
newChildAndDescendant = changeToNull;
|
|
1215
|
+
} else if (bestChildIndex === childIndex) {
|
|
1216
|
+
// the child is the best-child already
|
|
1217
|
+
// set it again to ensure that the best-descendent of the parent is updated
|
|
1218
|
+
newChildAndDescendant = changeToChild;
|
|
1219
|
+
} else {
|
|
1220
|
+
const bestChildNode = this.nodes[bestChildIndex];
|
|
1221
|
+
if (bestChildNode === undefined) {
|
|
1222
|
+
throw new ProtoArrayError({
|
|
1223
|
+
code: ProtoArrayErrorCode.INVALID_BEST_CHILD_INDEX,
|
|
1224
|
+
index: bestChildIndex,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
654
1227
|
|
|
655
|
-
|
|
1228
|
+
const bestChildLeadsToViableHead = this.nodeLeadsToViableHead(bestChildNode, currentSlot);
|
|
656
1229
|
|
|
657
|
-
|
|
658
|
-
|
|
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) {
|
|
1230
|
+
if (childLeadsToViableHead && !bestChildLeadsToViableHead) {
|
|
1231
|
+
// the child leads to a viable head, but the current best-child doesn't
|
|
666
1232
|
newChildAndDescendant = changeToChild;
|
|
667
|
-
|
|
1233
|
+
break outer;
|
|
1234
|
+
}
|
|
1235
|
+
if (!childLeadsToViableHead && bestChildLeadsToViableHead) {
|
|
1236
|
+
// the best child leads to a viable head but the child doesn't
|
|
668
1237
|
newChildAndDescendant = noChange;
|
|
1238
|
+
break outer;
|
|
669
1239
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1240
|
+
// Both nodes lead to viable heads (or both don't), need to pick winner
|
|
1241
|
+
|
|
1242
|
+
// Pre-fulu we pick whichever has higher weight, tie-breaker by root
|
|
1243
|
+
// Post-fulu we pick whichever has higher weight, then tie-breaker by root, then tie-breaker by `getPayloadStatusTiebreaker`
|
|
1244
|
+
// Gloas: nodes from previous slot (n-1) with EMPTY/FULL variant have weight hardcoded to 0.
|
|
1245
|
+
// https://github.com/ethereum/consensus-specs/blob/69a2582d5d62c914b24894bdb65f4bd5d4e49ae4/specs/gloas/fork-choice.md?plain=1#L442
|
|
1246
|
+
const childEffectiveWeight =
|
|
1247
|
+
!isGloasBlock(childNode) ||
|
|
1248
|
+
childNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1249
|
+
childNode.slot + 1 !== currentSlot
|
|
1250
|
+
? childNode.weight
|
|
1251
|
+
: 0;
|
|
1252
|
+
const bestChildEffectiveWeight =
|
|
1253
|
+
!isGloasBlock(bestChildNode) ||
|
|
1254
|
+
bestChildNode.payloadStatus === PayloadStatus.PENDING ||
|
|
1255
|
+
bestChildNode.slot + 1 !== currentSlot
|
|
1256
|
+
? bestChildNode.weight
|
|
1257
|
+
: 0;
|
|
1258
|
+
|
|
1259
|
+
if (childEffectiveWeight !== bestChildEffectiveWeight) {
|
|
1260
|
+
// Different effective weights, choose the winner by weight
|
|
1261
|
+
newChildAndDescendant = childEffectiveWeight >= bestChildEffectiveWeight ? changeToChild : noChange;
|
|
1262
|
+
break outer;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (childNode.blockRoot !== bestChildNode.blockRoot) {
|
|
1266
|
+
// Different blocks, tie-breaker by root
|
|
1267
|
+
newChildAndDescendant = childNode.blockRoot >= bestChildNode.blockRoot ? changeToChild : noChange;
|
|
1268
|
+
break outer;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Same effective weight and same root — Gloas EMPTY vs FULL from n-1, tie-breaker by payload status
|
|
1272
|
+
// Note: pre-Gloas, each child node of a block has a unique root, so this point should not be reached
|
|
1273
|
+
const childTiebreaker = this.getPayloadStatusTiebreaker(childNode, currentSlot, proposerBoostRoot);
|
|
1274
|
+
const bestChildTiebreaker = this.getPayloadStatusTiebreaker(bestChildNode, currentSlot, proposerBoostRoot);
|
|
1275
|
+
|
|
1276
|
+
if (childTiebreaker > bestChildTiebreaker) {
|
|
673
1277
|
newChildAndDescendant = changeToChild;
|
|
1278
|
+
} else if (childTiebreaker < bestChildTiebreaker) {
|
|
1279
|
+
newChildAndDescendant = noChange;
|
|
674
1280
|
} else {
|
|
1281
|
+
// Equal in all aspects, noChange
|
|
675
1282
|
newChildAndDescendant = noChange;
|
|
676
1283
|
}
|
|
677
1284
|
}
|
|
1285
|
+
} else if (childLeadsToViableHead) {
|
|
1286
|
+
// There is no current best-child and the child is viable.
|
|
1287
|
+
newChildAndDescendant = changeToChild;
|
|
1288
|
+
} else {
|
|
1289
|
+
// There is no current best-child but the child is not viable.
|
|
1290
|
+
newChildAndDescendant = noChange;
|
|
678
1291
|
}
|
|
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
1292
|
}
|
|
686
1293
|
|
|
687
1294
|
parentNode.bestChild = newChildAndDescendant[0];
|
|
@@ -761,13 +1368,14 @@ export class ProtoArray {
|
|
|
761
1368
|
}
|
|
762
1369
|
|
|
763
1370
|
const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch);
|
|
764
|
-
|
|
1371
|
+
const ancestorNode = this.getAncestorOrNull(node.blockRoot, finalizedSlot);
|
|
1372
|
+
return this.finalizedEpoch === 0 || (ancestorNode !== null && this.finalizedRoot === ancestorNode.blockRoot);
|
|
765
1373
|
}
|
|
766
1374
|
|
|
767
1375
|
/**
|
|
768
1376
|
* Same to getAncestor but it may return null instead of throwing error
|
|
769
1377
|
*/
|
|
770
|
-
getAncestorOrNull(blockRoot: RootHex, ancestorSlot: Slot):
|
|
1378
|
+
getAncestorOrNull(blockRoot: RootHex, ancestorSlot: Slot): ProtoNode | null {
|
|
771
1379
|
try {
|
|
772
1380
|
return this.getAncestor(blockRoot, ancestorSlot);
|
|
773
1381
|
} catch (_) {
|
|
@@ -776,49 +1384,116 @@ export class ProtoArray {
|
|
|
776
1384
|
}
|
|
777
1385
|
|
|
778
1386
|
/**
|
|
779
|
-
* Returns the
|
|
1387
|
+
* Returns the node identifier of an ancestor of `blockRoot` at the given `slot`.
|
|
780
1388
|
* (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
|
|
781
1389
|
*
|
|
782
1390
|
* NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
|
|
783
1391
|
*
|
|
784
1392
|
* ### Specification
|
|
785
1393
|
*
|
|
786
|
-
*
|
|
1394
|
+
* Modified for Gloas to return node identifier instead of just root:
|
|
1395
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#modified-get_ancestor
|
|
787
1396
|
*
|
|
788
|
-
*
|
|
1397
|
+
* Pre-Gloas: Returns (root, PAYLOAD_STATUS_FULL)
|
|
1398
|
+
* Gloas: Returns (root, payloadStatus) based on actual node state
|
|
789
1399
|
*/
|
|
790
|
-
getAncestor(blockRoot: RootHex, ancestorSlot: Slot):
|
|
791
|
-
|
|
792
|
-
|
|
1400
|
+
getAncestor(blockRoot: RootHex, ancestorSlot: Slot): ProtoNode {
|
|
1401
|
+
// Get any variant to check the block (use variants[0])
|
|
1402
|
+
const variantOrArr = this.indices.get(blockRoot);
|
|
1403
|
+
if (variantOrArr == null) {
|
|
793
1404
|
throw new ForkChoiceError({
|
|
794
1405
|
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
|
|
795
1406
|
root: blockRoot,
|
|
796
1407
|
});
|
|
797
1408
|
}
|
|
798
1409
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1410
|
+
const blockIndex = Array.isArray(variantOrArr) ? variantOrArr[0] : variantOrArr;
|
|
1411
|
+
const block = this.nodes[blockIndex];
|
|
1412
|
+
|
|
1413
|
+
// If block is at or before queried slot, return PENDING variant (or FULL for pre-Gloas)
|
|
1414
|
+
if (block.slot <= ancestorSlot) {
|
|
1415
|
+
// For pre-Gloas: only FULL exists at variants[0]
|
|
1416
|
+
// For Gloas: PENDING is at variants[0]
|
|
1417
|
+
return block;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Walk backwards through beacon blocks to find ancestor
|
|
1421
|
+
// Start with the parent of the current block
|
|
1422
|
+
let currentBlock = block;
|
|
1423
|
+
const parentVariants = this.indices.get(currentBlock.parentRoot);
|
|
1424
|
+
if (parentVariants == null) {
|
|
1425
|
+
throw new ForkChoiceError({
|
|
1426
|
+
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
1427
|
+
descendantRoot: blockRoot,
|
|
1428
|
+
ancestorSlot,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
let parentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
|
|
1433
|
+
let parentBlock = this.nodes[parentIndex];
|
|
1434
|
+
|
|
1435
|
+
// Walk backwards while parent.slot > ancestorSlot
|
|
1436
|
+
while (parentBlock.slot > ancestorSlot) {
|
|
1437
|
+
currentBlock = parentBlock;
|
|
1438
|
+
|
|
1439
|
+
const nextParentVariants = this.indices.get(currentBlock.parentRoot);
|
|
1440
|
+
if (nextParentVariants == null) {
|
|
1441
|
+
throw new ForkChoiceError({
|
|
1442
|
+
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
1443
|
+
descendantRoot: blockRoot,
|
|
1444
|
+
ancestorSlot,
|
|
1445
|
+
});
|
|
806
1446
|
}
|
|
1447
|
+
|
|
1448
|
+
parentIndex = Array.isArray(nextParentVariants) ? nextParentVariants[0] : nextParentVariants;
|
|
1449
|
+
parentBlock = this.nodes[parentIndex];
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Now parentBlock.slot <= ancestorSlot
|
|
1453
|
+
// Return the parent with the correct payload status based on currentBlock
|
|
1454
|
+
if (!isGloasBlock(currentBlock)) {
|
|
1455
|
+
// Pre-Gloas: return FULL variant (only one that exists)
|
|
1456
|
+
return parentBlock;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// Gloas: determine which parent variant (EMPTY or FULL) based on parent_block_hash
|
|
1460
|
+
const parentPayloadStatus = this.getParentPayloadStatus(currentBlock);
|
|
1461
|
+
const parentVariantIndex = this.getNodeIndexByRootAndStatus(currentBlock.parentRoot, parentPayloadStatus);
|
|
1462
|
+
|
|
1463
|
+
if (parentVariantIndex === undefined) {
|
|
807
1464
|
throw new ForkChoiceError({
|
|
808
1465
|
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
|
|
809
1466
|
descendantRoot: blockRoot,
|
|
810
1467
|
ancestorSlot,
|
|
811
1468
|
});
|
|
812
1469
|
}
|
|
813
|
-
|
|
814
|
-
return
|
|
1470
|
+
|
|
1471
|
+
return this.nodes[parentVariantIndex];
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Get the parent node index for traversal
|
|
1476
|
+
* For Gloas blocks: returns the correct EMPTY/FULL variant based on parent payload status
|
|
1477
|
+
* For pre-Gloas blocks: returns the simple parent index
|
|
1478
|
+
* Returns undefined if parent doesn't exist or can't be found
|
|
1479
|
+
*/
|
|
1480
|
+
private getParentNodeIndex(node: ProtoNode): number | undefined {
|
|
1481
|
+
if (isGloasBlock(node)) {
|
|
1482
|
+
// Use getParentPayloadStatus for Gloas blocks to get correct EMPTY/FULL variant
|
|
1483
|
+
const parentPayloadStatus = this.getParentPayloadStatus(node);
|
|
1484
|
+
return this.getNodeIndexByRootAndStatus(node.parentRoot, parentPayloadStatus);
|
|
1485
|
+
}
|
|
1486
|
+
// Simple parent traversal for pre-Gloas blocks (includes fork transition)
|
|
1487
|
+
return node.parent;
|
|
815
1488
|
}
|
|
816
1489
|
|
|
817
1490
|
/**
|
|
818
1491
|
* Iterate from a block root backwards over nodes
|
|
1492
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1493
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
819
1494
|
*/
|
|
820
|
-
*iterateAncestorNodes(blockRoot: RootHex): IterableIterator<ProtoNode> {
|
|
821
|
-
const startIndex = this.
|
|
1495
|
+
*iterateAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): IterableIterator<ProtoNode> {
|
|
1496
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
822
1497
|
if (startIndex === undefined) {
|
|
823
1498
|
return;
|
|
824
1499
|
}
|
|
@@ -835,20 +1510,30 @@ export class ProtoArray {
|
|
|
835
1510
|
}
|
|
836
1511
|
|
|
837
1512
|
/**
|
|
838
|
-
* Iterate from a
|
|
1513
|
+
* Iterate from a node backwards over ancestor nodes
|
|
1514
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1515
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
1516
|
+
* Handles fork transition from Gloas to pre-Gloas blocks
|
|
839
1517
|
*/
|
|
840
1518
|
*iterateAncestorNodesFromNode(node: ProtoNode): IterableIterator<ProtoNode> {
|
|
841
1519
|
while (node.parent !== undefined) {
|
|
842
|
-
|
|
1520
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1521
|
+
if (parentIndex === undefined) {
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
node = this.nodes[parentIndex];
|
|
843
1526
|
yield node;
|
|
844
1527
|
}
|
|
845
1528
|
}
|
|
846
1529
|
|
|
847
1530
|
/**
|
|
848
1531
|
* Get all nodes from a block root backwards
|
|
1532
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1533
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
849
1534
|
*/
|
|
850
|
-
getAllAncestorNodes(blockRoot: RootHex): ProtoNode[] {
|
|
851
|
-
const startIndex = this.
|
|
1535
|
+
getAllAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoNode[] {
|
|
1536
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
852
1537
|
if (startIndex === undefined) {
|
|
853
1538
|
return [];
|
|
854
1539
|
}
|
|
@@ -861,10 +1546,20 @@ export class ProtoArray {
|
|
|
861
1546
|
});
|
|
862
1547
|
}
|
|
863
1548
|
|
|
864
|
-
|
|
1549
|
+
// Exclude PENDING variant from returned ancestors.
|
|
1550
|
+
const nodes: ProtoNode[] = [];
|
|
1551
|
+
|
|
1552
|
+
if (node.payloadStatus !== PayloadStatus.PENDING) {
|
|
1553
|
+
nodes.push(node);
|
|
1554
|
+
}
|
|
865
1555
|
|
|
866
1556
|
while (node.parent !== undefined) {
|
|
867
|
-
|
|
1557
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1558
|
+
if (parentIndex === undefined) {
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
node = this.nodes[parentIndex];
|
|
868
1563
|
nodes.push(node);
|
|
869
1564
|
}
|
|
870
1565
|
|
|
@@ -875,9 +1570,12 @@ export class ProtoArray {
|
|
|
875
1570
|
* The opposite of iterateNodes.
|
|
876
1571
|
* iterateNodes is to find ancestor nodes of a blockRoot.
|
|
877
1572
|
* this is to find non-ancestor nodes of a blockRoot.
|
|
1573
|
+
*
|
|
1574
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1575
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
878
1576
|
*/
|
|
879
|
-
getAllNonAncestorNodes(blockRoot: RootHex): ProtoNode[] {
|
|
880
|
-
const startIndex = this.
|
|
1577
|
+
getAllNonAncestorNodes(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoNode[] {
|
|
1578
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
881
1579
|
if (startIndex === undefined) {
|
|
882
1580
|
return [];
|
|
883
1581
|
}
|
|
@@ -889,24 +1587,39 @@ export class ProtoArray {
|
|
|
889
1587
|
index: startIndex,
|
|
890
1588
|
});
|
|
891
1589
|
}
|
|
1590
|
+
|
|
1591
|
+
// For both Gloas and pre-Gloas blocks
|
|
892
1592
|
const result: ProtoNode[] = [];
|
|
893
1593
|
let nodeIndex = startIndex;
|
|
894
1594
|
while (node.parent !== undefined) {
|
|
895
|
-
const parentIndex = node
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1595
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1596
|
+
if (parentIndex === undefined) {
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
node = this.nodes[parentIndex];
|
|
1601
|
+
// Collect non-ancestor nodes between current and parent
|
|
1602
|
+
// Filter to exclude PENDING nodes (FULL variant pre-gloas, EMPTY or FULL variant post-gloas)
|
|
1603
|
+
result.push(
|
|
1604
|
+
...this.getNodesBetween(nodeIndex, parentIndex).filter((n) => n.payloadStatus !== PayloadStatus.PENDING)
|
|
1605
|
+
);
|
|
899
1606
|
nodeIndex = parentIndex;
|
|
900
1607
|
}
|
|
901
|
-
|
|
1608
|
+
// Collect remaining nodes from nodeIndex to beginning
|
|
1609
|
+
result.push(...this.getNodesBetween(nodeIndex, 0).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
902
1610
|
return result;
|
|
903
1611
|
}
|
|
904
1612
|
|
|
905
1613
|
/**
|
|
906
1614
|
* Returns both ancestor and non-ancestor nodes in a single traversal.
|
|
1615
|
+
* For Gloas blocks: returns EMPTY/FULL variants (not PENDING) based on parent payload status
|
|
1616
|
+
* For pre-Gloas blocks: returns FULL variants
|
|
907
1617
|
*/
|
|
908
|
-
getAllAncestorAndNonAncestorNodes(
|
|
909
|
-
|
|
1618
|
+
getAllAncestorAndNonAncestorNodes(
|
|
1619
|
+
blockRoot: RootHex,
|
|
1620
|
+
payloadStatus: PayloadStatus
|
|
1621
|
+
): {ancestors: ProtoNode[]; nonAncestors: ProtoNode[]} {
|
|
1622
|
+
const startIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
910
1623
|
if (startIndex === undefined) {
|
|
911
1624
|
return {ancestors: [], nonAncestors: []};
|
|
912
1625
|
}
|
|
@@ -922,39 +1635,73 @@ export class ProtoArray {
|
|
|
922
1635
|
const ancestors: ProtoNode[] = [];
|
|
923
1636
|
const nonAncestors: ProtoNode[] = [];
|
|
924
1637
|
|
|
1638
|
+
// Include starting node if it's not PENDING (i.e., pre-Gloas or EMPTY/FULL variant post-Gloas)
|
|
1639
|
+
if (node.payloadStatus !== PayloadStatus.PENDING) {
|
|
1640
|
+
ancestors.push(node);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
925
1643
|
let nodeIndex = startIndex;
|
|
926
1644
|
while (node.parent !== undefined) {
|
|
927
|
-
|
|
1645
|
+
const parentIndex = this.getParentNodeIndex(node);
|
|
1646
|
+
if (parentIndex === undefined) {
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
928
1649
|
|
|
929
|
-
|
|
930
|
-
|
|
1650
|
+
node = this.nodes[parentIndex];
|
|
1651
|
+
ancestors.push(node);
|
|
931
1652
|
|
|
932
|
-
//
|
|
933
|
-
|
|
1653
|
+
// Collect non-ancestor nodes between current and parent
|
|
1654
|
+
// Filter to exclude PENDING nodes (include all FULL/EMPTY for both pre-Gloas and Gloas)
|
|
1655
|
+
nonAncestors.push(
|
|
1656
|
+
...this.getNodesBetween(nodeIndex, parentIndex).filter((n) => n.payloadStatus !== PayloadStatus.PENDING)
|
|
1657
|
+
);
|
|
934
1658
|
nodeIndex = parentIndex;
|
|
935
1659
|
}
|
|
936
1660
|
|
|
937
|
-
|
|
938
|
-
nonAncestors.push(...this.getNodesBetween(nodeIndex, 0));
|
|
1661
|
+
// Collect remaining non-ancestor nodes from nodeIndex to beginning
|
|
1662
|
+
nonAncestors.push(...this.getNodesBetween(nodeIndex, 0).filter((n) => n.payloadStatus !== PayloadStatus.PENDING));
|
|
939
1663
|
|
|
940
1664
|
return {ancestors, nonAncestors};
|
|
941
1665
|
}
|
|
942
1666
|
|
|
1667
|
+
/**
|
|
1668
|
+
* Check if a block exists in the proto array
|
|
1669
|
+
* Uses default variant (PENDING for Gloas, FULL for pre-Gloas)
|
|
1670
|
+
*/
|
|
943
1671
|
hasBlock(blockRoot: RootHex): boolean {
|
|
944
|
-
return this.
|
|
1672
|
+
return this.getDefaultNodeIndex(blockRoot) !== undefined;
|
|
945
1673
|
}
|
|
946
1674
|
|
|
947
|
-
|
|
948
|
-
|
|
1675
|
+
/**
|
|
1676
|
+
* Return ProtoNode for blockRoot with explicit payload status
|
|
1677
|
+
*
|
|
1678
|
+
* @param blockRoot - The block root to look up
|
|
1679
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1680
|
+
* @returns The ProtoNode for the specified variant, or undefined if not found
|
|
1681
|
+
*
|
|
1682
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1683
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1684
|
+
*/
|
|
1685
|
+
getNode(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoNode | undefined {
|
|
1686
|
+
const blockIndex = this.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
|
|
949
1687
|
if (blockIndex === undefined) {
|
|
950
1688
|
return undefined;
|
|
951
1689
|
}
|
|
952
1690
|
return this.getNodeByIndex(blockIndex);
|
|
953
1691
|
}
|
|
954
1692
|
|
|
955
|
-
/**
|
|
956
|
-
|
|
957
|
-
|
|
1693
|
+
/**
|
|
1694
|
+
* Return MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1695
|
+
*
|
|
1696
|
+
* @param blockRoot - The block root to look up
|
|
1697
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1698
|
+
* @returns The ProtoBlock for the specified variant (spreads properties), or undefined if not found
|
|
1699
|
+
*
|
|
1700
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1701
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1702
|
+
*/
|
|
1703
|
+
getBlock(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock | undefined {
|
|
1704
|
+
const node = this.getNode(blockRoot, payloadStatus);
|
|
958
1705
|
if (!node) {
|
|
959
1706
|
return undefined;
|
|
960
1707
|
}
|
|
@@ -963,9 +1710,19 @@ export class ProtoArray {
|
|
|
963
1710
|
};
|
|
964
1711
|
}
|
|
965
1712
|
|
|
966
|
-
/**
|
|
967
|
-
|
|
968
|
-
|
|
1713
|
+
/**
|
|
1714
|
+
* Return NON-MUTABLE ProtoBlock for blockRoot with explicit payload status
|
|
1715
|
+
*
|
|
1716
|
+
* @param blockRoot - The block root to look up
|
|
1717
|
+
* @param payloadStatus - The specific payload status variant (PENDING/EMPTY/FULL)
|
|
1718
|
+
* @returns The ProtoBlock for the specified variant (does not spread properties)
|
|
1719
|
+
* @throws Error if block not found
|
|
1720
|
+
*
|
|
1721
|
+
* Note: Callers must explicitly specify which variant they need.
|
|
1722
|
+
* Use getDefaultVariant() to get the canonical variant for a block.
|
|
1723
|
+
*/
|
|
1724
|
+
getBlockReadonly(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock {
|
|
1725
|
+
const node = this.getNode(blockRoot, payloadStatus);
|
|
969
1726
|
if (!node) {
|
|
970
1727
|
throw Error(`No block for root ${blockRoot}`);
|
|
971
1728
|
}
|
|
@@ -975,23 +1732,28 @@ export class ProtoArray {
|
|
|
975
1732
|
/**
|
|
976
1733
|
* Returns `true` if the `descendantRoot` has an ancestor with `ancestorRoot`.
|
|
977
1734
|
* Always returns `false` if either input roots are unknown.
|
|
978
|
-
* Still returns `true` if `ancestorRoot` === `descendantRoot`
|
|
1735
|
+
* Still returns `true` if `ancestorRoot` === `descendantRoot` and payload statuses match.
|
|
979
1736
|
*/
|
|
980
|
-
isDescendant(
|
|
981
|
-
|
|
1737
|
+
isDescendant(
|
|
1738
|
+
ancestorRoot: RootHex,
|
|
1739
|
+
ancestorPayloadStatus: PayloadStatus,
|
|
1740
|
+
descendantRoot: RootHex,
|
|
1741
|
+
descendantPayloadStatus: PayloadStatus
|
|
1742
|
+
): boolean {
|
|
1743
|
+
const ancestorNode = this.getNode(ancestorRoot, ancestorPayloadStatus);
|
|
982
1744
|
if (!ancestorNode) {
|
|
983
1745
|
return false;
|
|
984
1746
|
}
|
|
985
1747
|
|
|
986
|
-
if (ancestorRoot === descendantRoot) {
|
|
1748
|
+
if (ancestorRoot === descendantRoot && ancestorPayloadStatus === descendantPayloadStatus) {
|
|
987
1749
|
return true;
|
|
988
1750
|
}
|
|
989
1751
|
|
|
990
|
-
for (const node of this.iterateAncestorNodes(descendantRoot)) {
|
|
1752
|
+
for (const node of this.iterateAncestorNodes(descendantRoot, descendantPayloadStatus)) {
|
|
991
1753
|
if (node.slot < ancestorNode.slot) {
|
|
992
1754
|
return false;
|
|
993
1755
|
}
|
|
994
|
-
if (node.blockRoot === ancestorNode.blockRoot) {
|
|
1756
|
+
if (node.blockRoot === ancestorNode.blockRoot && node.payloadStatus === ancestorNode.payloadStatus) {
|
|
995
1757
|
return true;
|
|
996
1758
|
}
|
|
997
1759
|
}
|