import { isTreeBacked , readonlyValues , toHexString , TreeBacked } from "@chainsafe/ssz" ; import { SAFE_SLOTS_TO_UPDATE_JUSTIFIED , SLOTS_PER_HISTORICAL_ROOT } from "@chainsafe/lodestar-params" ; import { Slot , ValidatorIndex , phase0 , allForks , ssz , RootHex , Epoch , Root } from "@chainsafe/lodestar-types" ; import { computeSlotsSinceEpochStart , computeStartSlotAtEpoch , computeEpochAtSlot , ZERO_HASH , bellatrix , } from "@chainsafe/lodestar-beacon-state-transition" ; import { IChainConfig , IChainForkConfig } from "@chainsafe/lodestar-config" ; import { computeDeltas } from "../protoArray/computeDeltas" ; import { HEX_ZERO_HASH , IVoteTracker , IProtoBlock , ExecutionStatus } from "../protoArray/interface" ; import { ProtoArray } from "../protoArray/protoArray" ; import { IForkChoiceMetrics } from "../metrics" ; import { ForkChoiceError , ForkChoiceErrorCode , InvalidBlockCode , InvalidAttestationCode } from "./errors" ; import { IForkChoice , ILatestMessage , IQueuedAttestation , OnBlockPrecachedData } from "./interface" ; import { IForkChoiceStore , CheckpointWithHex , toCheckpointWithHex } from "./store" ; /**
 * Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
 *
 * https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/fork-choice.md#ethereum-20-phase-0----beacon-chain-fork-choice
 *
 * ## Detail
 *
 * This class wraps `ProtoArray` and provides:
 *
 * - Management of validators latest messages and balances
 * - Management of the justified/finalized checkpoints as seen by fork choice
 * - Queuing of attestations from the current slot
 *
 * This class MUST be used with the following considerations:
 *
 * - Time is not updated automatically, updateTime MUST be called every slot
 */ export class ForkChoice implements IForkChoice { /**
     * Votes currently tracked in the protoArray
     * Indexed by validator index
     * Each vote contains the latest message and previous message
     */ private readonly votes : IVoteTracker [ ] = [ ] ; /**
     * Attestations that arrived at the current slot and must be queued for later processing.
     * NOT currently tracked in the protoArray
     */ private readonly queuedAttestations = new Set < IQueuedAttestation > ( ) ; /**
     * Balances tracked in the protoArray, or soon to be tracked
     * Indexed by validator index
     *
     * This should be the balances of the state at fcStore.bestJustifiedCheckpoint
     */ private bestJustifiedBalances : number [ ] ; /** Avoid having to compute detas all the times. */ private synced = false ; /** Cached head */ private head : IProtoBlock ; /**
     * Only cache attestation data root hex if it's tree backed since it's available.
     **/ private validatedAttestationDatas = new Set < string > ( ) ; /**
     * Instantiates a Fork Choice from some existing components
     *
     * This is useful if the existing components have been loaded from disk after a process restart.
     */ constructor ( private readonly config , private readonly fcStore , /** The underlying representation of the block DAG. */ private readonly protoArray , /**
     * Balances currently tracked in the protoArray
     * Indexed by validator index
     *
     * This should be the balances of the state at fcStore.justifiedCheckpoint
     */ private justifiedBalances , private readonly metrics ? ) { this . bestJustifiedBalances = justifiedBalances ; this . head = this . updateHead ( ) ; } /**
     * Returns the block root of an ancestor of `blockRoot` at the given `slot`.
     * (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
     *
     * NOTE: May be expensive: potentially walks through the entire fork of head to finalized block
     *
     * ### Specification
     *
     * Equivalent to:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_ancestor
     */ getAncestor ( blockRoot , ancestorSlot ) { const block = this . protoArray . getBlock ( blockRoot ) ; if ( ! block ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . MISSING_PROTO_ARRAY_BLOCK , root : blockRoot , } ) ; } if ( block . slot > ancestorSlot ) { for ( const node of this . protoArray . iterateAncestorNodes ( blockRoot ) ) { if ( node . slot <= ancestorSlot ) { return node . blockRoot ; } } throw new ForkChoiceError ( { code : ForkChoiceErrorCode . UNKNOWN_ANCESTOR , descendantRoot : blockRoot , ancestorSlot , } ) ; } else { return blockRoot ; } } /**
     * Get the cached head root
     */ getHeadRoot ( ) { return this . getHead ( ) . blockRoot ; } /**
     * Get the cached head
     */ getHead ( ) { return this . head ; } /**
     * Run the fork choice rule to determine the head.
     * Update the head cache.
     *
     * Very expensive function (400ms / run as of Aug 2021). Call when the head really needs to be re-calculated.
     *
     * ## Specification
     *
     * Is equivalent to:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/fork-choice.md#get_head
     */ updateHead ( ) { let timer ; this . metrics ?. forkChoiceRequests . inc ( ) ; try { if ( ! this . synced ) { timer = this . metrics ?. forkChoiceFindHead . startTimer ( ) ; const deltas = computeDeltas ( this . protoArray . indices , this . votes , this . justifiedBalances , this . justifiedBalances ) ; this . protoArray . applyScoreChanges ( { deltas , justifiedEpoch : this . fcStore . justifiedCheckpoint . epoch , justifiedRoot : this . fcStore . justifiedCheckpoint . rootHex , finalizedEpoch : this . fcStore . finalizedCheckpoint . epoch , finalizedRoot : this . fcStore . finalizedCheckpoint . rootHex , } ) ; this . synced = true ; } const headRoot = this . protoArray . findHead ( this . fcStore . justifiedCheckpoint . rootHex ) ; const headIndex = this . protoArray . indices . get ( headRoot ) ; if ( headIndex === undefined ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . MISSING_PROTO_ARRAY_BLOCK , root : headRoot , } ) ; } const headNode = this . protoArray . nodes [ headIndex ] ; if ( headNode === undefined ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . MISSING_PROTO_ARRAY_BLOCK , root : headRoot , } ) ; } return ( this . head = headNode ) ; } catch ( e ) { this . metrics ?. forkChoiceErrors . inc ( ) ; throw e ; } finally { if ( timer ) timer ( ) ; } } /** Very expensive function, iterates the entire ProtoArray. Called only in debug API */ getHeads ( ) { return this . protoArray . nodes . filter ( ( node ) => node . bestChild === undefined ) ; } getFinalizedCheckpoint ( ) { return this . fcStore . finalizedCheckpoint ; } getJustifiedCheckpoint ( ) { return this . fcStore . justifiedCheckpoint ; } getBestJustifiedCheckpoint ( ) { return this . fcStore . bestJustifiedCheckpoint ; } /**
     * Add `block` to the fork choice DAG.
     *
     * ## Specification
     *
     * Approximates:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_block
     *
     * It only approximates the specification since it does not run the `state_transition` check.
     * That should have already been called upstream and it's too expensive to call again.
     *
     * ## Notes:
     *
     * The supplied block **must** pass the `state_transition` function as it will not be run here.
     *
     * `justifiedBalances` balances of justified state which is updated synchronously.
     * This ensures that the forkchoice is never out of sync.
     */ onBlock ( block , state , preCachedData ? ) { const { parentRoot , slot } = block ; const parentRootHex = toHexString ( parentRoot ) ; if ( ! this . protoArray . hasBlock ( parentRootHex ) ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_BLOCK , err : { code : InvalidBlockCode . UNKNOWN_PARENT , root : parentRootHex , } , } ) ; } if ( slot > this . fcStore . currentSlot ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_BLOCK , err : { code : InvalidBlockCode . FUTURE_SLOT , currentSlot : this . fcStore . currentSlot , blockSlot : slot , } , } ) ; } const finalizedSlot = computeStartSlotAtEpoch ( this . fcStore . finalizedCheckpoint . epoch ) ; if ( slot <= finalizedSlot ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_BLOCK , err : { code : InvalidBlockCode . FINALIZED_SLOT , finalizedSlot , blockSlot : slot , } , } ) ; } const blockAncestorRoot = this . getAncestor ( parentRootHex , finalizedSlot ) ; const finalizedRoot = this . fcStore . finalizedCheckpoint . rootHex ; if ( blockAncestorRoot !== finalizedRoot ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_BLOCK , err : { code : InvalidBlockCode . NOT_FINALIZED_DESCENDANT , finalizedRoot , blockAncestor : blockAncestorRoot , } , } ) ; } if ( preCachedData ?. isMergeTransitionBlock || ( bellatrix . isBellatrixStateType ( state ) && bellatrix . isBellatrixBlockBodyType ( block . body ) && bellatrix . isMergeTransitionBlock ( state , block . body ) ) ) assertValidTerminalPowBlock ( this . config , ( block as unknown ) as bellatrix . BeaconBlock , preCachedData ) ; let shouldUpdateJustified = false ; const { finalizedCheckpoint } = state ; const currentJustifiedCheckpoint = toCheckpointWithHex ( state . currentJustifiedCheckpoint ) ; const stateJustifiedEpoch = currentJustifiedCheckpoint . epoch ; if ( stateJustifiedEpoch > this . fcStore . justifiedCheckpoint . epoch ) { const { justifiedBalances } = preCachedData || { } ; if ( ! justifiedBalances ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . UNABLE_TO_SET_JUSTIFIED_CHECKPOINT , error : new Error ( "No validator balances supplied" ) , } ) ; } if ( stateJustifiedEpoch > this . fcStore . bestJustifiedCheckpoint . epoch ) { this . updateBestJustified ( currentJustifiedCheckpoint , justifiedBalances ) ; } if ( this . shouldUpdateJustifiedCheckpoint ( state ) ) { shouldUpdateJustified = true ; } } if ( finalizedCheckpoint . epoch > this . fcStore . finalizedCheckpoint . epoch ) { this . fcStore . finalizedCheckpoint = toCheckpointWithHex ( finalizedCheckpoint ) ; shouldUpdateJustified = true ; this . synced = false ; } if ( shouldUpdateJustified ) { const { justifiedBalances } = preCachedData || { } ; if ( ! justifiedBalances ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . UNABLE_TO_SET_JUSTIFIED_CHECKPOINT , error : new Error ( "No validator balances supplied" ) , } ) ; } this . updateJustified ( currentJustifiedCheckpoint , justifiedBalances ) ; } const targetSlot = computeStartSlotAtEpoch ( computeEpochAtSlot ( slot ) ) ; const blockRoot = this . config . getForkTypes ( slot ) . BeaconBlock . hashTreeRoot ( block ) ; const targetRoot = slot === targetSlot ? blockRoot : state . blockRoots [ targetSlot % SLOTS_PER_HISTORICAL_ROOT ] ; this . protoArray . onBlock ( { slot : slot , blockRoot : toHexString ( blockRoot ) , parentRoot : parentRootHex , targetRoot : toHexString ( targetRoot ) , stateRoot : toHexString ( block . stateRoot ) , justifiedEpoch : stateJustifiedEpoch , justifiedRoot : toHexString ( state . currentJustifiedCheckpoint . root ) , finalizedEpoch : finalizedCheckpoint . epoch , finalizedRoot : toHexString ( state . finalizedCheckpoint . root ) , ... ( bellatrix . isBellatrixBlockBodyType ( block . body ) && bellatrix . isBellatrixStateType ( state ) && bellatrix . isExecutionEnabled ( state , block . body ) ? { executionPayloadBlockHash : toHexString ( block . body . executionPayload . blockHash ) , executionStatus : this . getPostMergeExecStatus ( preCachedData ) , } : { executionPayloadBlockHash : null , executionStatus : this . getPreMergeExecStatus ( preCachedData ) } ) , } ) ; } /**
     * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
     *
     * ## Specification
     *
     * Approximates:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_attestation
     *
     * It only approximates the specification since it does not perform
     * `is_valid_indexed_attestation` since that should already have been called upstream and it's
     * too expensive to call again.
     *
     * ## Notes:
     *
     * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
     * will not be run here.
     */ onAttestation ( attestation ) { const attestationData = attestation . data ; const { slot , beaconBlockRoot } = attestationData ; const blockRootHex = toHexString ( beaconBlockRoot ) ; const targetEpoch = attestationData . target . epoch ; if ( ssz . Root . equals ( beaconBlockRoot , ZERO_HASH ) ) { return ; } this . validateOnAttestation ( attestation , slot , blockRootHex , targetEpoch ) ; if ( slot < this . fcStore . currentSlot ) { for ( const validatorIndex of readonlyValues ( attestation . attestingIndices ) ) { this . addLatestMessage ( validatorIndex , targetEpoch , blockRootHex ) ; } } else { this . queuedAttestations . add ( { slot : slot , attestingIndices : Array . from ( readonlyValues ( attestation . attestingIndices ) ) , blockRoot : blockRootHex , targetEpoch , } ) ; } } getLatestMessage ( validatorIndex ) { const vote = this . votes [ validatorIndex ] ; if ( vote === undefined ) { return undefined ; } return { epoch : vote . nextEpoch , root : vote . nextRoot , } ; } /**
     * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`.
     */ updateTime ( currentSlot ) { while ( this . fcStore . currentSlot < currentSlot ) { const previousSlot = this . fcStore . currentSlot ; this . onTick ( previousSlot + 1 ) ; } this . processAttestationQueue ( ) ; this . validatedAttestationDatas = new Set ( ) ; } getTime ( ) { return this . fcStore . currentSlot ; } /** Returns `true` if the block is known **and** a descendant of the finalized root. */ hasBlock ( blockRoot ) { return this . hasBlockHex ( toHexString ( blockRoot ) ) ; } /** Returns a `IProtoBlock` if the block is known **and** a descendant of the finalized root. */ getBlock ( blockRoot ) { return this . getBlockHex ( toHexString ( blockRoot ) ) ; } /**
     * Returns `true` if the block is known **and** a descendant of the finalized root.
     */ hasBlockHex ( blockRoot ) { return this . protoArray . hasBlock ( blockRoot ) && this . isDescendantOfFinalized ( blockRoot ) ; } /**
     * Returns a `IProtoBlock` if the block is known **and** a descendant of the finalized root.
     */ getBlockHex ( blockRoot ) { const block = this . protoArray . getBlock ( blockRoot ) ; if ( ! block ) { return null ; } if ( ! this . isDescendantOfFinalized ( blockRoot ) ) { return null ; } return block ; } getJustifiedBlock ( ) { const block = this . getBlockHex ( this . fcStore . justifiedCheckpoint . rootHex ) ; if ( ! block ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . MISSING_PROTO_ARRAY_BLOCK , root : this . fcStore . justifiedCheckpoint . rootHex , } ) ; } return block ; } getFinalizedBlock ( ) { const block = this . getBlockHex ( this . fcStore . finalizedCheckpoint . rootHex ) ; if ( ! block ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . MISSING_PROTO_ARRAY_BLOCK , root : this . fcStore . finalizedCheckpoint . rootHex , } ) ; } return block ; } /**
     * Return `true` if `block_root` is equal to the finalized root, or a known descendant of it.
     */ isDescendantOfFinalized ( blockRoot ) { return this . protoArray . isDescendant ( this . fcStore . finalizedCheckpoint . rootHex , blockRoot ) ; } /**
     * Returns true if the `descendantRoot` has an ancestor with `ancestorRoot`.
     *
     * Always returns `false` if either input roots are unknown.
     * Still returns `true` if `ancestorRoot===descendantRoot` (and the roots are known)
     */ isDescendant ( ancestorRoot , descendantRoot ) { return this . protoArray . isDescendant ( ancestorRoot , descendantRoot ) ; } prune ( finalizedRoot ) { return this . protoArray . maybePrune ( finalizedRoot ) ; } setPruneThreshold ( threshold ) { this . protoArray . pruneThreshold = threshold ; } /**
     * Iterates backwards through block summaries, starting from a block root.
     * Return only the non-finalized blocks.
     */ iterateAncestorBlocks ( blockRoot ) { return this . protoArray . iterateAncestorNodes ( blockRoot ) ; } /**
     * Returns all blocks backwards starting from a block root.
     * Return only the non-finalized blocks.
     */ getAllAncestorBlocks ( blockRoot ) { const blocks = this . protoArray . getAllAncestorNodes ( blockRoot ) ; return blocks . slice ( 0 , blocks . length - 1 ) ; } /**
     * The same to iterateAncestorBlocks but this gets non-ancestor nodes instead of ancestor nodes.
     */ getAllNonAncestorBlocks ( blockRoot ) { return this . protoArray . getAllNonAncestorNodes ( blockRoot ) ; } getCanonicalBlockAtSlot ( slot ) { if ( slot >= this . head . slot ) { return this . head ; } for ( const block of this . protoArray . iterateAncestorNodes ( this . head . blockRoot ) ) { if ( block . slot === slot ) { return block ; } } return null ; } /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */ forwarditerateAncestorBlocks ( ) { return this . protoArray . nodes ; } /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */ getBlockSummariesByParentRoot ( parentRoot ) { return this . protoArray . nodes . filter ( ( node ) => node . parentRoot === parentRoot ) ; } /** Very expensive function, iterates the entire ProtoArray. TODO: Is this function even necessary? */ getBlockSummariesAtSlot ( slot ) { const nodes = this . protoArray . nodes ; const blocksAtSlot = [ ] ; for ( let i = 0 , len = nodes . length ; i < len ; i ++ ) { const node = nodes [ i ] ; if ( node . slot === slot ) { blocksAtSlot . push ( node ) ; } } return blocksAtSlot ; } /** Returns the distance of common ancestor of nodes to newNode. Returns null if newNode is descendant of prevNode */ getCommonAncestorDistance ( prevBlock , newBlock ) { const prevNode = this . protoArray . getNode ( prevBlock . blockRoot ) ; const newNode = this . protoArray . getNode ( newBlock . blockRoot ) ; if ( ! prevNode ) throw Error ( `No node if forkChoice for blockRoot ${ prevBlock . blockRoot }` ) ; if ( ! newNode ) throw Error ( `No node if forkChoice for blockRoot ${ newBlock . blockRoot }` ) ; const commonAncestor = this . protoArray . getCommonAncestor ( prevNode , newNode ) ; if ( ! commonAncestor ) return null ; if ( commonAncestor . blockRoot === prevNode . blockRoot || commonAncestor . blockRoot === newNode . blockRoot ) { return null ; } return newNode . slot - commonAncestor . slot ; } /**
     * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidate till hash provided
     * TODO: implementation:
     * 1. verify is_merge_block if the mergeblock has not yet been validated
     * 2. Throw critical error and exit if a block in finalized chain gets invalidated
     */ validateLatestHash ( _latestValidHash , _invalidateTillHash ) { return ; } private getPreMergeExecStatus ( preCachedData ? ) { const executionStatus = preCachedData ?. executionStatus || ExecutionStatus . PreMerge ; if ( executionStatus !== ExecutionStatus . PreMerge ) throw Error ( `Invalid pre-merge execution status: expected: ${ ExecutionStatus . PreMerge }, got ${ executionStatus }` ) ; return executionStatus ; } private getPostMergeExecStatus ( preCachedData ? ) { const executionStatus = preCachedData ?. executionStatus || ExecutionStatus . Syncing ; if ( executionStatus === ExecutionStatus . PreMerge ) throw Error ( `Invalid post-merge execution status: expected: ${ ExecutionStatus . Syncing } or ${ ExecutionStatus . Valid } , got ${ executionStatus }` ) ; return executionStatus ; } private updateJustified ( justifiedCheckpoint , justifiedBalances ) { this . synced = false ; this . justifiedBalances = justifiedBalances ; this . fcStore . justifiedCheckpoint = justifiedCheckpoint ; } private updateBestJustified ( justifiedCheckpoint , justifiedBalances ) { this . bestJustifiedBalances = justifiedBalances ; this . fcStore . bestJustifiedCheckpoint = justifiedCheckpoint ; } /**
     * Returns `true` if the given `store` should be updated to set
     * `state.current_justified_checkpoint` its `justified_checkpoint`.
     *
     * ## Specification
     *
     * Is equivalent to:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#should_update_justified_checkpoint
     */ private shouldUpdateJustifiedCheckpoint ( state ) { const { slot , currentJustifiedCheckpoint } = state ; const newJustifiedCheckpoint = currentJustifiedCheckpoint ; if ( computeSlotsSinceEpochStart ( this . fcStore . currentSlot ) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED ) { return true ; } const justifiedSlot = computeStartSlotAtEpoch ( this . fcStore . justifiedCheckpoint . epoch ) ; if ( justifiedSlot >= slot ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . ATTEMPT_TO_REVERT_JUSTIFICATION , store : justifiedSlot , state : slot , } ) ; } if ( computeEpochAtSlot ( slot ) < computeEpochAtSlot ( this . fcStore . currentSlot ) ) { return true ; } const justifiedAncestor = this . getAncestor ( toHexString ( newJustifiedCheckpoint . root ) , justifiedSlot ) ; if ( justifiedAncestor !== this . fcStore . justifiedCheckpoint . rootHex ) { return false ; } return true ; } /**
     * Validates the `indexed_attestation` for application to fork choice.
     *
     * ## Specification
     *
     * Equivalent to:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#validate_on_attestation
     */ private validateOnAttestation ( indexedAttestation , slot , blockRootHex , targetEpoch ) { if ( ! indexedAttestation . attestingIndices . length ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . EMPTY_AGGREGATION_BITFIELD , } , } ) ; } const attestationData = indexedAttestation . data ; if ( isTreeBacked ( attestationData ) && this . validatedAttestationDatas . has ( toHexString ( ( ( attestationData as unknown ) as TreeBacked < phase0 . AttestationData > ) . tree . root ) ) ) { return ; } this . validateAttestationData ( indexedAttestation . data , slot , blockRootHex , targetEpoch ) ; } private validateAttestationData ( attestationData , slot , beaconBlockRootHex , targetEpoch ) { const epochNow = computeEpochAtSlot ( this . fcStore . currentSlot ) ; const targetRootHex = toHexString ( attestationData . target . root ) ; if ( targetEpoch > epochNow ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . FUTURE_EPOCH , attestationEpoch : targetEpoch , currentEpoch : epochNow , } , } ) ; } else if ( targetEpoch + 1 < epochNow ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . PAST_EPOCH , attestationEpoch : targetEpoch , currentEpoch : epochNow , } , } ) ; } if ( targetEpoch !== computeEpochAtSlot ( slot ) ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . BAD_TARGET_EPOCH , target : targetEpoch , slot , } , } ) ; } if ( this . fcStore . currentSlot < slot + 1 ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . FUTURE_SLOT , attestationSlot : slot , latestPermissibleSlot : this . fcStore . currentSlot - 1 , } , } ) ; } if ( ! this . protoArray . hasBlock ( targetRootHex ) ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . UNKNOWN_TARGET_ROOT , root : targetRootHex , } , } ) ; } const block = this . protoArray . getBlock ( beaconBlockRootHex ) ; if ( ! block ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . UNKNOWN_HEAD_BLOCK , beaconBlockRoot : beaconBlockRootHex , } , } ) ; } const expectedTargetHex = targetEpoch > computeEpochAtSlot ( block . slot ) ? beaconBlockRootHex : block . targetRoot ; if ( expectedTargetHex !== targetRootHex ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . INVALID_TARGET , attestation : targetRootHex , local : expectedTargetHex , } , } ) ; } if ( block . slot > slot ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INVALID_ATTESTATION , err : { code : InvalidAttestationCode . ATTESTS_TO_FUTURE_BLOCK , block : block . slot , attestation : slot , } , } ) ; } if ( isTreeBacked ( attestationData ) ) { this . validatedAttestationDatas . add ( toHexString ( ( ( attestationData as unknown ) as TreeBacked < phase0 . AttestationData > ) . tree . root ) ) ; } } /**
     * Add a validator's latest message to the tracked votes
     */ private addLatestMessage ( validatorIndex , nextEpoch , nextRoot ) { this . synced = false ; const vote = this . votes [ validatorIndex ] ; if ( vote === undefined ) { this . votes [ validatorIndex ] = { currentRoot : HEX_ZERO_HASH , nextRoot , nextEpoch , } ; } else if ( nextEpoch > vote . nextEpoch ) { vote . nextRoot = nextRoot ; vote . nextEpoch = nextEpoch ; } } /**
     * Processes and removes from the queue any queued attestations which may now be eligible for
     * processing due to the slot clock incrementing.
     */ private processAttestationQueue ( ) { const currentSlot = this . fcStore . currentSlot ; for ( const attestation of this . queuedAttestations . values ( ) ) { if ( attestation . slot <= currentSlot ) { this . queuedAttestations . delete ( attestation ) ; const { blockRoot , targetEpoch } = attestation ; const blockRootHex = blockRoot ; for ( const validatorIndex of attestation . attestingIndices ) { this . addLatestMessage ( validatorIndex , targetEpoch , blockRootHex ) ; } } } } /**
     * Called whenever the current time increases.
     *
     * ## Specification
     *
     * Equivalent to:
     *
     * https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#on_tick
     */ private onTick ( time ) { const previousSlot = this . fcStore . currentSlot ; if ( time > previousSlot + 1 ) { throw new ForkChoiceError ( { code : ForkChoiceErrorCode . INCONSISTENT_ON_TICK , previousSlot , time , } ) ; } this . fcStore . currentSlot = time ; const currentSlot = time ; if ( computeSlotsSinceEpochStart ( currentSlot ) !== 0 ) { return ; } const { bestJustifiedCheckpoint , justifiedCheckpoint , finalizedCheckpoint } = this . fcStore ; if ( bestJustifiedCheckpoint . epoch > justifiedCheckpoint . epoch ) { const finalizedSlot = computeStartSlotAtEpoch ( finalizedCheckpoint . epoch ) ; const ancestorAtFinalizedSlot = this . getAncestor ( bestJustifiedCheckpoint . rootHex , finalizedSlot ) ; if ( ancestorAtFinalizedSlot === finalizedCheckpoint . rootHex ) { this . updateJustified ( this . fcStore . bestJustifiedCheckpoint , this . bestJustifiedBalances ) ; } } } } function assertValidTerminalPowBlock ( config , block , preCachedData ? ) { if ( ! ssz . Root . equals ( config . TERMINAL_BLOCK_HASH , ZERO_HASH ) ) { if ( computeEpochAtSlot ( block . slot ) < config . TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH ) throw Error ( `Terminal block activation epoch ${ config . TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH } not reached` ) ; if ( ! ssz . Root . equals ( block . body . executionPayload . parentHash , config . TERMINAL_BLOCK_HASH ) ) throw new Error ( `Invalid terminal block hash, expected: ${ toHexString ( config . TERMINAL_BLOCK_HASH ) }, actual: ${ toHexString ( block . body . executionPayload . parentHash ) }` ) ; } else { if ( preCachedData ?. executionStatus === ExecutionStatus . Syncing ) return ; const { powBlock , powBlockParent } = preCachedData || { } ; if ( ! powBlock ) throw Error ( "onBlock preCachedData must include powBlock" ) ; if ( ! powBlockParent ) throw Error ( "onBlock preCachedData must include powBlock" ) ; const isTotalDifficultyReached = powBlock . totalDifficulty >= config . TERMINAL_TOTAL_DIFFICULTY ; const isParentTotalDifficultyValid = powBlockParent . totalDifficulty < config . TERMINAL_TOTAL_DIFFICULTY ; if ( ! isTotalDifficultyReached || ! isParentTotalDifficultyValid ) throw Error ( `Invalid terminal POW block: total difficulty not reached ${ powBlockParent . totalDifficulty } < ${ powBlock . totalDifficulty }` ) ; } }