Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,13 @@ export class ForkChoice implements IForkChoice {
return null;
}

// Pre-Gloas
if (!Array.isArray(variantIndices)) {
const node = this.protoArray.nodes[variantIndices];
return node.executionPayloadBlockHash === blockHash ? node : null;
}

// Post-Gloas
for (const variantIndex of variantIndices) {
const node = this.protoArray.nodes[variantIndex];
if (node.executionPayloadBlockHash === blockHash) {
Expand Down Expand Up @@ -1243,6 +1250,8 @@ export class ForkChoice implements IForkChoice {
return this.protoArray.nodes;
}

// TODO: this function is ambiguous, consumer should also provide payload, or it should accept a ProtoBlock instead
// also consumer may want PENDING or EMPTY only
*forwardIterateDescendants(blockRoot: RootHex): IterableIterator<ProtoBlock> {
const rootsInChain = new Set([blockRoot]);

Expand All @@ -1255,7 +1264,9 @@ export class ForkChoice implements IForkChoice {
}

// Find the minimum index among all variants to start iteration
const blockIndex = Math.min(...blockVariants.filter((idx) => idx !== undefined));
const blockIndex = Array.isArray(blockVariants)
? Math.min(...blockVariants.filter((idx) => idx !== undefined))
: blockVariants;

for (let i = blockIndex + 1; i < this.protoArray.nodes.length; i++) {
const node = this.protoArray.nodes[i];
Expand Down
128 changes: 71 additions & 57 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ type ProposerBoost = {root: RootHex; score: number};

const ZERO_HASH_HEX = toRootHex(Buffer.alloc(32, 0));

/** Pre-Gloas: single element, FULL index (for backward compatibility) */
type PreGloasVariantIndex = number;
/**
* Post-Gloas: array length is 2 or 3
* - Length 2: [PENDING_INDEX, EMPTY_INDEX] when payload hasn't arrived yet
* - Length 3: [PENDING_INDEX, EMPTY_INDEX, FULL_INDEX] when payload has arrived
*/
type GloasVariantIndices = [number, number] | [number, number, number];
type VariantIndices = PreGloasVariantIndex | GloasVariantIndices;

export class ProtoArray {
// Do not attempt to prune the tree unless it has at least this many nodes.
// Small prunes simply waste time
Expand All @@ -42,14 +52,9 @@ export class ProtoArray {
* - number[1] = EMPTY variant index (PayloadStatus.EMPTY = 1)
* - number[2] = FULL variant index (PayloadStatus.FULL = 2)
*
* Pre-Gloas: array length is 1, number[0] contains FULL index (for backward compatibility)
* Post-Gloas: array length is 2 or 3
* - Length 2: [PENDING_INDEX, EMPTY_INDEX] when payload hasn't arrived yet
* - Length 3: [PENDING_INDEX, EMPTY_INDEX, FULL_INDEX] when payload has arrived
*
* Note: undefined array elements indicate that variant doesn't exist for this block
*/
indices = new Map<RootHex, number[]>();
indices = new Map<RootHex, VariantIndices>();
lvhError?: LVHExecError;

private previousProposerBoost: ProposerBoost | null = null;
Expand Down Expand Up @@ -117,16 +122,16 @@ export class ProtoArray {
* Note: payloadStatus is required. Use getDefaultVariant() to get the canonical variant.
*/
getNodeIndexByRootAndStatus(root: RootHex, payloadStatus: PayloadStatus): number | undefined {
const variants = this.indices.get(root);
if (!variants) {
const variantOrArr = this.indices.get(root);
if (variantOrArr == null) {
return undefined;
}

// Pre-Gloas: only one variant exists (FULL at index 0)
if (variants.length === 1) {
// Pre-Gloas: only FULL variant exists
if (!Array.isArray(variantOrArr)) {
// Return FULL variant if no status specified or FULL explicitly requested
if (payloadStatus === PayloadStatus.FULL) {
return variants[0];
return variantOrArr;
}
// PENDING and EMPTY are invalid for pre-Gloas blocks
throw new ProtoArrayError({
Expand All @@ -136,7 +141,7 @@ export class ProtoArray {
}

// Gloas: return the specified variant, or PENDING if not specified
return variants[payloadStatus];
return variantOrArr[payloadStatus];
}

/**
Expand All @@ -148,13 +153,13 @@ export class ProtoArray {
* @returns PayloadStatus.FULL for pre-Gloas, PayloadStatus.PENDING for Gloas, undefined if block not found
*/
getDefaultVariant(blockRoot: RootHex): PayloadStatus | undefined {
const variants = this.indices.get(blockRoot);
if (!variants) {
const variantOrArr = this.indices.get(blockRoot);
if (variantOrArr == null) {
return undefined;
}

// Pre-Gloas: only one variant exists (FULL)
if (variants.length === 1) {
// Pre-Gloas: only FULL variant exists
if (!Array.isArray(variantOrArr)) {
return PayloadStatus.FULL;
}

Expand All @@ -179,23 +184,23 @@ export class ProtoArray {
}

// Gloas block must have parentBlockHash from its SignedExecutionPayloadBid
const parentBlockHash = block.parentBlockHash;
if (parentBlockHash === null) {
// should not happen for Gloas blocks
return PayloadStatus.FULL;
}

// Get parent node to compare execution payload hash
// Use variants[0] which works for both pre-Gloas (FULL) and Gloas (PENDING)
const parentVariants = this.indices.get(block.parentRoot);
if (!parentVariants) {
if (parentVariants == null) {
// Parent not found
throw new ProtoArrayError({
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
root: block.parentRoot,
});
}

const parentBlockHash = block.parentBlockHash;
// Pre-Gloas blocks don't have parentBlockHash
if (parentBlockHash === null || !Array.isArray(parentVariants)) {
return PayloadStatus.FULL;
}

const parentIndex = parentVariants[0];
const parentExecutionHash = this.nodes[parentIndex].executionPayloadBlockHash;

Expand Down Expand Up @@ -361,8 +366,8 @@ export class ProtoArray {

// Check if parent exists by getting variants array
const parentVariants = this.indices.get(block.parentRoot);
if (parentVariants) {
const anyParentIndex = parentVariants[0];
if (parentVariants != null) {
const anyParentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
const anyParentNode = this.nodes[anyParentIndex];

if (!isGloasBlock(anyParentNode)) {
Expand Down Expand Up @@ -404,10 +409,7 @@ export class ProtoArray {

// Store both variants in the indices array
// [PENDING, EMPTY, undefined] - FULL will be added later if payload arrives
const variants: number[] = [];
variants[PayloadStatus.PENDING] = pendingIndex;
variants[PayloadStatus.EMPTY] = emptyIndex;
this.indices.set(block.blockRoot, variants);
this.indices.set(block.blockRoot, [pendingIndex, emptyIndex]);

// Update bestChild pointers
if (parentIndex !== undefined) {
Expand Down Expand Up @@ -438,9 +440,8 @@ export class ProtoArray {
const nodeIndex = this.nodes.length;
this.nodes.push(node);

// Store FULL variant in indices array
// Pre-Gloas: variants[0] contains FULL index
this.indices.set(block.blockRoot, [nodeIndex]);
// Pre-Gloas: store FULL index instead of array
this.indices.set(block.blockRoot, nodeIndex);

// If this node is valid, lets propagate the valid status up the chain
// and throw error if we counter invalid, as this breaks consensus
Expand Down Expand Up @@ -470,15 +471,15 @@ export class ProtoArray {
): void {
// First check if block exists
const variants = this.indices.get(blockRoot);
if (!variants) {
if (variants == null) {
// Equivalent to `assert envelope.beacon_block_root in store.block_states`
throw new ProtoArrayError({
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
root: blockRoot,
});
}

if (variants.length === 1) {
if (!Array.isArray(variants)) {
// Pre-gloas block should not be calling this method
throw new ProtoArrayError({
code: ProtoArrayErrorCode.PRE_GLOAS_BLOCK,
Expand Down Expand Up @@ -978,15 +979,17 @@ export class ProtoArray {
*/
maybePrune(finalizedRoot: RootHex): ProtoBlock[] {
const variants = this.indices.get(finalizedRoot);
if (!variants) {
if (variants == null) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN,
root: finalizedRoot,
});
}

// Find the minimum index among all variants to ensure we don't prune too much
const finalizedIndex = Math.min(...variants.filter((idx) => idx !== undefined));
const finalizedIndex = Array.isArray(variants)
? Math.min(...variants.filter((idx) => idx !== undefined))
: variants;

if (finalizedIndex < this.pruneThreshold) {
// Pruning at small numbers incurs more cost than benefit
Expand Down Expand Up @@ -1017,24 +1020,35 @@ export class ProtoArray {
this.nodes = this.nodes.slice(finalizedIndex);

// Adjust the indices map - subtract finalizedIndex from all node indices
const newIndices = new Map<RootHex, number[]>();
for (const [root, variantIndices] of this.indices.entries()) {
const adjustedVariants: number[] = [];
for (let i = 0; i < variantIndices.length; i++) {
const idx = variantIndices[i];
if (idx !== undefined) {
if (idx < finalizedIndex) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
value: "indices",
});
}
adjustedVariants[i] = idx - finalizedIndex;
// Pre-Gloas: single index
if (!Array.isArray(variantIndices)) {
if (variantIndices < finalizedIndex) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
value: "indices",
});
}
this.indices.set(root, variantIndices - finalizedIndex);
continue;
}
newIndices.set(root, adjustedVariants);

// Post-Gloas: array of variant indices
const adjustedVariants = variantIndices.map((variantIndex) => {
if (variantIndex === undefined) {
return undefined;
}

if (variantIndex < finalizedIndex) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INDEX_OVERFLOW,
value: "indices",
});
}
return variantIndex - finalizedIndex;
});
this.indices.set(root, adjustedVariants as GloasVariantIndices);
}
this.indices = newIndices;

// Iterate through all the existing nodes and adjust their indices to match the new layout of this.nodes
for (let i = 0, len = this.nodes.length; i < len; i++) {
Expand Down Expand Up @@ -1346,15 +1360,15 @@ export class ProtoArray {
*/
getAncestor(blockRoot: RootHex, ancestorSlot: Slot): ProtoNode {
// Get any variant to check the block (use variants[0])
const variants = this.indices.get(blockRoot);
if (!variants) {
const variantOrArr = this.indices.get(blockRoot);
if (variantOrArr == null) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK,
root: blockRoot,
});
}

const blockIndex = variants[0];
const blockIndex = Array.isArray(variantOrArr) ? variantOrArr[0] : variantOrArr;
const block = this.nodes[blockIndex];

// If block is at or before queried slot, return PENDING variant (or FULL for pre-Gloas)
Expand All @@ -1368,31 +1382,31 @@ export class ProtoArray {
// Start with the parent of the current block
let currentBlock = block;
const parentVariants = this.indices.get(currentBlock.parentRoot);
if (!parentVariants) {
if (parentVariants == null) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
descendantRoot: blockRoot,
ancestorSlot,
});
}

let parentIndex = parentVariants[0];
let parentIndex = Array.isArray(parentVariants) ? parentVariants[0] : parentVariants;
let parentBlock = this.nodes[parentIndex];

// Walk backwards while parent.slot > ancestorSlot
while (parentBlock.slot > ancestorSlot) {
currentBlock = parentBlock;

const nextParentVariants = this.indices.get(currentBlock.parentRoot);
if (!nextParentVariants) {
if (nextParentVariants == null) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.UNKNOWN_ANCESTOR,
descendantRoot: blockRoot,
ancestorSlot,
});
}

parentIndex = nextParentVariants[0];
parentIndex = Array.isArray(nextParentVariants) ? nextParentVariants[0] : nextParentVariants;
parentBlock = this.nodes[parentIndex];
}

Expand Down
46 changes: 21 additions & 25 deletions packages/fork-choice/test/unit/protoArray/gloas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,22 @@ describe("Gloas Fork Choice", () => {
blockRoot: RootHex,
payloadStatus: PayloadStatus
): ProtoNode | undefined {
const variants = (protoArray as any).indices.get(blockRoot);
if (!variants) return undefined;

// For pre-Gloas, variants[0] contains FULL index
if (variants.length === 1) {
// Pre-Gloas block only has FULL variant
// Only return if requested payloadStatus is FULL
if (payloadStatus === PayloadStatus.FULL) {
return (protoArray as any).nodes[variants[0]];
}
return undefined;
}

// For post-Gloas, variants[payloadStatus] contains the index for that status
const index = variants[payloadStatus];
// const variants = (protoArray as any).indices.get(blockRoot);
// if (!variants) return undefined;

// // For pre-Gloas, variants[0] contains FULL index
// if (variants.length === 1) {
// // Pre-Gloas block only has FULL variant
// // Only return if requested payloadStatus is FULL
// if (payloadStatus === PayloadStatus.FULL) {
// return (protoArray as any).nodes[variants[0]];
// }
// return undefined;
// }

// // For post-Gloas, variants[payloadStatus] contains the index for that status
// const index = variants[payloadStatus];
const index = protoArray.getNodeIndexByRootAndStatus(blockRoot, payloadStatus);
if (index === undefined) return undefined;
return (protoArray as any).nodes[index];
}
Expand Down Expand Up @@ -77,9 +78,8 @@ describe("Gloas Fork Choice", () => {
const protoArray = ProtoArray.initialize(createTestBlock(0, genesisRoot, "0x00"), 0);
const variants = (protoArray as any).indices.get(genesisRoot);
expect(variants).toBeDefined();
// Pre-Gloas: variants[0] contains FULL index
expect(variants.length).toBe(1);
expect(variants[0]).toBe(0);
// Pre-Gloas: variants is the FULL index
expect(variants).toBe(0);
});

it("getNodeByPayloadStatus() retrieves correct variants", () => {
Expand Down Expand Up @@ -130,11 +130,8 @@ describe("Gloas Fork Choice", () => {
expect(fullNode?.payloadStatus).toBe(PayloadStatus.FULL);

// Should not have PENDING or EMPTY variants
const pendingNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.PENDING);
expect(pendingNode).toBeUndefined();

const emptyNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.EMPTY);
expect(emptyNode).toBeUndefined();
expect(() => getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.PENDING)).toThrow();
expect(() => getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.EMPTY)).toThrow();
});

it("getNode() finds pre-Gloas blocks by root (FULL)", () => {
Expand Down Expand Up @@ -216,8 +213,7 @@ describe("Gloas Fork Choice", () => {
const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL);
expect(fullNode).toBeDefined();

const pendingNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.PENDING);
expect(pendingNode).toBeUndefined();
expect(() => getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.PENDING)).toThrow();
});
});

Expand Down
Loading