Fork choice defines how an honest node should follow the chain and determine the current head. This page outlines Prysm's fork choice implementation, which differs slightly from the spec due to various optimizations and tradeoffs. One can think of fork choice as representing a tree of blocks and a set of votes determining which branch is the head.
The following documentation refers to the implementation at https://github.com/prysmaticlabs/prysm/tree/develop/beacon-chain/forkchoice
In Prysm's implementation, a store object consists of:
An individual object consists of:
exposes three main functions to the outside world to compute the head of the chain
Callers can input newer validator votes into the store either on demand as unseen verified attestations arrive or at intervals after attestations are batched and aggregated. The caller provides:
The function updates the store's list of by replacing the at each given with the new and , but only if the new is higher than the existing one.
When inserting a block into , the user provides a and its corresponding . The justified and finalized epochs are extracted from the , while the , , and are extracted from the . If the block is post-Bellatrix, the execution is also extracted from the payload.
The function first checks whether the node already exists. If not, a new node is created and marked as by default.
The checkpoint is determined as follows:
The node is then inserted into key-value stores, both by and by .
If the node has no parent and the tree root is not already set, it becomes the .
For nodes with a parent:
Finally, the function calls updateBestDescendant
(see explanation below under Compute New Head) starting from the tree’s root node, using the current justified, finalized, and current epochs to refresh fork choice viability.
A caller can call compute new head at any time. Typically, callers invoke this after a new block is processed or at intervals when sufficient attestations have been handled and they want an updated view of the head of the chain. Compute new head can be broken down into the following sub-functions:
Update balances is a critical part of the algorithm that determines how validator balances are applied to nodes in the tree. This method takes no input directly. Instead, it operates based on the already cached validator and in the object.
We loop through each and determine whether it affects any node’s . Slashed validators are ignored. An update occurs only if the validator’s voted has changed or the validator’s has changed.
If the in the vote points to a known node (and is not the zero hash), we increase that node’s . If a validator is moving their vote from one node to another, we subtract the from the old node.
Finally, fork choice rotates the validator vote so that the becomes the and the new becomes the current .
Update proposer boost score adjusts some node balances to reflect proposer boost for the current slot. If a previous proposer boost was applied, its is reduced by the previous boost score. Next, we apply the proposer boost to the current node, identified by the cached . The boost is calculated as:
Where is the total active validator balance divided by the number of slots per epoch, and (default 40%) is a configuration parameter.
After applying the boost, we rotate the current proposer boost root and score to the previous ones in preparation for the next update. Calling apply proposer boost multiple times is safe because of the subtraction and addition.
Update weight is a recursive function that updates the weight for a given node and all its children under the fork choice tree. The weight is used to determine what is the head. When the function gets called, it visits every child node and recurses down, aggregating the total weight from the children for bottom-up computation.
Update best descendant is one of the final functions called during head computation. It's a recursive function that determines the best descendant for each node in the fork choice tree based on weight and fork choice viability. Starting from the leaves, it traverses child nodes to identify those that lead to a viable head.
The function only considers nodes that can lead to a viable head. The recursive function leads to viable head checks whether a child node or any of its descendants is viable to be the head. A node is considered viable if its justified epoch matches the fork choice store’s justified epoch, or if it comes from an earlier epoch but its justification is still recent—within two epochs of the current one.
Among viable children, it selects the one with the highest weight, using the block root as a tie-breaker if weights are equal. The selected child or its best descendant is then assigned as the current node’s best descendant. If no viable child is found, the best descendant field is cleared.
The final get head function returns the of the current head block in the fork choice tree, starting from the justified checkpoint node. It first retrieves the node corresponding to , handling an edge case if it's the genesis checkpoint. It then selects the of the justified node, or the justified node itself if nothing exists. Before finalizing the head, it checks whether the chosen block is still viable based on justification and finalization rules. If the block is invalid, an error is returned and a flag is set. Otherwise, the function updates internal tracking and metrics and returns the new head