Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/fork-choice/src/forkChoice/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export enum InvalidBlockCode {
}

export type InvalidBlock =
| {code: InvalidBlockCode.UNKNOWN_PARENT; root: RootHex}
| {code: InvalidBlockCode.UNKNOWN_PARENT; root: RootHex; hash: RootHex | null}
| {code: InvalidBlockCode.FUTURE_SLOT; currentSlot: Slot; blockSlot: Slot}
| {code: InvalidBlockCode.FINALIZED_SLOT; finalizedSlot: Slot; blockSlot: Slot}
| {code: InvalidBlockCode.NOT_FINALIZED_DESCENDANT; finalizedRoot: RootHex; blockAncestor?: RootHex};
Expand Down
42 changes: 9 additions & 33 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,17 +594,18 @@ export class ForkChoice implements IForkChoice {
): ProtoBlock {
const {parentRoot, slot} = block;
const parentRootHex = toRootHex(parentRoot);
// Parent block must be known
// We do not care about the variant here, we just need to find the parent block
const defaultStatus = this.protoArray.getDefaultVariant(parentRootHex);
const parentBlock =
defaultStatus !== undefined ? this.protoArray.getBlock(parentRootHex, defaultStatus) : undefined;
// Parent block must be known because state_transition would have failed otherwise.
const parentHashHex = isGloasBeaconBlock(block)
? toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
: null;
const parentBlock = this.protoArray.getParent(parentRootHex, parentHashHex);
if (!parentBlock) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.INVALID_BLOCK,
err: {
code: InvalidBlockCode.UNKNOWN_PARENT,
root: parentRootHex,
hash: parentHashHex,
},
});
}
Expand Down Expand Up @@ -834,9 +835,7 @@ export class ForkChoice implements IForkChoice {
blockHashFromBid: isGloasBeaconBlock(block)
? toRootHex(block.body.signedExecutionPayloadBid.message.blockHash)
: null,
parentBlockHash: isGloasBeaconBlock(block)
? toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
: null,
parentBlockHash: parentHashHex,
};

this.protoArray.onBlock(protoBlock, currentSlot);
Expand Down Expand Up @@ -1089,33 +1088,10 @@ export class ForkChoice implements IForkChoice {
}

/**
* Returns a `ProtoBlock` that has matching block root and block hash
* Returns EMPTY or FULL `ProtoBlock` that has matching block root and block hash
*/
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
const variantIndices = this.protoArray.indices.get(blockRoot);
if (variantIndices === undefined) {
return null;
}

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

// Post-Gloas: Prioritize FULL > EMPTY > PENDING
// EMPTY and PENDING have the same block hash (parent hash), so we prefer EMPTY over PENDING
for (const status of [PayloadStatus.FULL, PayloadStatus.EMPTY, PayloadStatus.PENDING]) {
const variantIndex = variantIndices[status];
if (variantIndex !== undefined) {
const node = this.protoArray.nodes[variantIndex];
if (node.executionPayloadBlockHash === blockHash) {
return node;
}
}
}

return null;
return this.protoArray.getBlockHexAndBlockHash(blockRoot, blockHash);
}

getJustifiedBlock(): ProtoBlock {
Expand Down
2 changes: 2 additions & 0 deletions packages/fork-choice/src/protoArray/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ProtoArrayErrorCode {
FINALIZED_NODE_UNKNOWN = "PROTO_ARRAY_ERROR_FINALIZED_NODE_UNKNOWN",
JUSTIFIED_NODE_UNKNOWN = "PROTO_ARRAY_ERROR_JUSTIFIED_NODE_UNKNOWN",
UNKNOWN_BLOCK = "PROTO_ARRAY_ERROR_UNKNOWN_BLOCK",
UNKNOWN_PARENT_BLOCK = "PROTO_ARRAY_ERROR_UNKNOWN_PARENT_BLOCK",
INVALID_FINALIZED_ROOT_CHANGE = "PROTO_ARRAY_ERROR_INVALID_FINALIZED_ROOT_CHANGE",
INVALID_NODE_INDEX = "PROTO_ARRAY_ERROR_INVALID_NODE_INDEX",
INVALID_PARENT_INDEX = "PROTO_ARRAY_ERROR_INVALID_PARENT_INDEX",
Expand All @@ -35,6 +36,7 @@ export type ProtoArrayErrorType =
| {code: ProtoArrayErrorCode.FINALIZED_NODE_UNKNOWN; root: RootHex}
| {code: ProtoArrayErrorCode.JUSTIFIED_NODE_UNKNOWN; root: RootHex}
| {code: ProtoArrayErrorCode.UNKNOWN_BLOCK; root: RootHex}
| {code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK; parentRoot: RootHex; parentHash: RootHex | null}
| {code: ProtoArrayErrorCode.INVALID_FINALIZED_ROOT_CHANGE}
| {code: ProtoArrayErrorCode.INVALID_NODE_INDEX; index: number}
| {code: ProtoArrayErrorCode.INVALID_PARENT_INDEX; index: number}
Expand Down
5 changes: 5 additions & 0 deletions packages/fork-choice/src/protoArray/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export type MaybeValidExecutionStatus = Exclude<ExecutionStatus, ExecutionStatus

export type BlockExtraMeta =
| {
// Pre-gloas:
// - block hash of payload of the block
// Post-gloas:
// - this is parentBlockHash of block bid because payload is only received later
// - payload block hash for FULL variant
executionPayloadBlockHash: RootHex;
executionPayloadNumber: UintNum64;
executionStatus: Exclude<ExecutionStatus, ExecutionStatus.PreMerge>;
Expand Down
96 changes: 74 additions & 22 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,44 +170,96 @@ export class ProtoArray {
/**
* Determine which parent payload status a block extends
* Spec: gloas/fork-choice.md#new-get_parent_payload_status
* def get_parent_payload_status(store: Store, block: BeaconBlock) -> PayloadStatus:
* parent = store.blocks[block.parent_root]
* parent_block_hash = block.body.signed_execution_payload_bid.message.parent_block_hash
* message_block_hash = parent.body.signed_execution_payload_bid.message.block_hash
* return PAYLOAD_STATUS_FULL if parent_block_hash == message_block_hash else PAYLOAD_STATUS_EMPTY
*
* Compares parent_block_hash in child's bid with executionPayloadBlockHash in parent:
* - Match → child extends FULL parent (parent has payload)
* - No match → child extends EMPTY parent (parent has no payload)
* In lodestar forkchoice, we don't store the full bid, so we compares parent_block_hash in child's bid with executionPayloadBlockHash in parent:
* - If it matches EMPTY variant, return EMPTY
* - If it matches FULL variant, return FULL
* - If no match, throw UNKNOWN_PARENT_BLOCK error
*
* For pre-Gloas blocks: always returns FULL
*/
getParentPayloadStatus(block: ProtoBlock): PayloadStatus {
// Pre-Gloas blocks have payloads embedded, so parents are always FULL
if (!isGloasBlock(block)) {
const {parentBlockHash} = block;
if (parentBlockHash === null) {
return PayloadStatus.FULL;
}

// Gloas block must have parentBlockHash from its SignedExecutionPayloadBid
// 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 == null) {
// Parent not found
const parentBlock = this.getBlockHexAndBlockHash(block.parentRoot, parentBlockHash);
if (parentBlock == null) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.UNKNOWN_BLOCK,
root: block.parentRoot,
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
parentRoot: block.parentRoot,
parentHash: parentBlockHash,
});
}

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

/**
* Return the parent `ProtoBlock` given its root and block hash.
*/
getParent(parentRoot: RootHex, parentBlockHash: RootHex | null): ProtoBlock | null {
// pre-gloas
if (parentBlockHash === null) {
const parentIndex = this.indices.get(parentRoot);
if (parentIndex === undefined) {
return null;
}
if (Array.isArray(parentIndex)) {
// Gloas block found when pre-gloas expected
throw new ProtoArrayError({
code: ProtoArrayErrorCode.UNKNOWN_PARENT_BLOCK,
parentRoot,
parentHash: parentBlockHash,
});
}
return this.nodes[parentIndex] ?? null;
}

// post-gloas
return this.getBlockHexAndBlockHash(parentRoot, parentBlockHash);
}

/**
* Returns an EMPTY or FULL `ProtoBlock` that has matching block root and block hash
*/
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null {
const variantIndices = this.indices.get(blockRoot);
if (variantIndices === undefined) {
return null;
}

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

// Post-Gloas, check empty and full variants
const fullNodeIndex = variantIndices[PayloadStatus.FULL];
if (fullNodeIndex !== undefined) {
const fullNode = this.nodes[fullNodeIndex];
if (fullNode && fullNode.executionPayloadBlockHash === blockHash) {
return fullNode;
}
}

const emptyNode = this.nodes[variantIndices[PayloadStatus.EMPTY]];
if (emptyNode && emptyNode.executionPayloadBlockHash === blockHash) {
return emptyNode;
}
Comment thread
twoeths marked this conversation as resolved.

const parentIndex = parentVariants[0];
const parentExecutionHash = this.nodes[parentIndex].executionPayloadBlockHash;
// PENDING is the same to EMPTY so not likely we can return it
// also it's only specific for fork-choice

// Compare parent_block_hash from child's bid with parent's execution payload hash
// Match means child extends FULL variant (parent has payload)
// No match means child extends EMPTY variant (parent has no payload)
return parentBlockHash === parentExecutionHash ? PayloadStatus.FULL : PayloadStatus.EMPTY;
return null;
}

/**
Expand Down
12 changes: 6 additions & 6 deletions packages/fork-choice/test/unit/protoArray/gloas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,17 +496,17 @@ describe("Gloas Fork Choice", () => {

it("inter-block: new PENDING extends parent's EMPTY or FULL", () => {
// Block A
const blockA = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot);
const blockA = createTestBlock(gloasForkSlot, "0x02Root", genesisRoot, genesisRoot);
protoArray.onBlock(blockA, gloasForkSlot);
protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot);
protoArray.onExecutionPayload("0x02Root", gloasForkSlot, "0x02Hash", gloasForkSlot, stateRoot);

// Block B extends A's FULL (parentBlockHash matches)
const blockB = createTestBlock(gloasForkSlot + 1, "0x03", "0x02", "0x02");
const blockB = createTestBlock(gloasForkSlot + 1, "0x03Root", "0x02Root", "0x02Hash");
protoArray.onBlock(blockB, gloasForkSlot + 1);

const blockAPending = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING);
const blockAFull = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.FULL);
const blockBPending = getNodeByPayloadStatus(protoArray, "0x03", PayloadStatus.PENDING);
const blockAPending = protoArray.getNodeIndexByRootAndStatus("0x02Root", PayloadStatus.PENDING);
const blockAFull = protoArray.getNodeIndexByRootAndStatus("0x02Root", PayloadStatus.FULL);
const blockBPending = getNodeByPayloadStatus(protoArray, "0x03Root", PayloadStatus.PENDING);

// Block B's PENDING should NOT point to A's PENDING
expect(blockBPending?.parent).not.toBe(blockAPending);
Expand Down
Loading