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