From ea6a6b1acd62a934136cef3f64fb3fee15826619 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Tue, 31 Jan 2023 13:35:01 +0100 Subject: [PATCH] track slot as part of fork choice debug API (#4565) Extends fork choice state to also track slot numbers to improve accuracy of `/eth/v1/debug/fork_choice` endpoint. Autoenable this API on devnet, and disable some extra checks on devnet to aid focused testing efforts. Align fork choice pruning logic with API based on checkpoints vs root. --- Makefile | 4 +- .../attestation_pool.nim | 2 +- .../consensus_object_pools/blockchain_dag.nim | 17 ++- beacon_chain/extras.nim | 2 + beacon_chain/fork_choice/fork_choice.nim | 68 +++++----- .../fork_choice/fork_choice_types.nim | 4 +- beacon_chain/fork_choice/proto_array.nim | 122 ++++++++++++------ beacon_chain/nimbus_beacon_node.nim | 14 +- beacon_chain/rpc/rest_debug_api.nim | 23 +--- tests/fork_choice/interpreter.nim | 14 +- tests/fork_choice/scenarios/ffg_01.nim | 12 +- tests/fork_choice/scenarios/ffg_02.nim | 40 ++++-- tests/fork_choice/scenarios/no_votes.nim | 24 +++- tests/fork_choice/scenarios/votes.nim | 48 +++++-- 14 files changed, 252 insertions(+), 142 deletions(-) diff --git a/Makefile b/Makefile index f6e2ab62d..613603c85 100644 --- a/Makefile +++ b/Makefile @@ -261,8 +261,8 @@ local-testnet-mainnet: # test binaries that can output an XML report XML_TEST_BINARIES_CORE := \ - consensus_spec_tests_mainnet \ - consensus_spec_tests_minimal + consensus_spec_tests_minimal \ + consensus_spec_tests_mainnet XML_TEST_BINARIES := \ $(XML_TEST_BINARIES_CORE) \ diff --git a/beacon_chain/consensus_object_pools/attestation_pool.nim b/beacon_chain/consensus_object_pools/attestation_pool.nim index c1e4af65a..9417a3aa9 100644 --- a/beacon_chain/consensus_object_pools/attestation_pool.nim +++ b/beacon_chain/consensus_object_pools/attestation_pool.nim @@ -134,7 +134,7 @@ proc init*(T: type AttestationPool, dag: ChainDAGRef, # and then to make sure the fork choice data structure doesn't grow # too big - getting an EpochRef can be expensive. forkChoice.backend.process_block( - blckRef.root, blckRef.parent.root, epochRef.checkpoints) + blckRef.bid, blckRef.parent.root, epochRef.checkpoints) else: epochRef = dag.getEpochRef(blckRef, blckRef.slot.epoch, false).expect( "Getting an EpochRef should always work for non-finalized blocks") diff --git a/beacon_chain/consensus_object_pools/blockchain_dag.nim b/beacon_chain/consensus_object_pools/blockchain_dag.nim index 0189761e6..5696b1343 100644 --- a/beacon_chain/consensus_object_pools/blockchain_dag.nim +++ b/beacon_chain/consensus_object_pools/blockchain_dag.nim @@ -79,15 +79,18 @@ template withUpdatedState*( else: failureBody -func get_effective_balances(validators: openArray[Validator], epoch: Epoch): - seq[Gwei] = +func get_effective_balances( + validators: openArray[Validator], + epoch: Epoch, + ignoreSlashed: bool): seq[Gwei] = ## Get the balances from a state as counted for fork choice result.newSeq(validators.len) # zero-init for i in 0 ..< result.len: # All non-active validators have a 0 balance let validator = unsafeAddr validators[i] - if validator[].is_active_validator(epoch): + if validator[].is_active_validator(epoch) and ( + ignoreSlashed or not validator[].slashed): result[i] = validator[].effective_balance proc updateValidatorKeys*(dag: ChainDAGRef, validators: openArray[Validator]) = @@ -569,7 +572,9 @@ func init*( epochRef.effective_balances_bytes = snappyEncode(SSZ.encode( List[Gwei, Limit VALIDATOR_REGISTRY_LIMIT]( - get_effective_balances(getStateField(state, validators).asSeq, epoch)))) + get_effective_balances( + getStateField(state, validators).asSeq, epoch, + experimental notin dag.updateFlags)))) epochRef @@ -898,7 +903,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB, cfg.checkForkConsistency() doAssert updateFlags - { - strictVerification, enableTestFeatures, lowParticipation + strictVerification, experimental, enableTestFeatures, lowParticipation } == {}, "Other flags not supported in ChainDAG" # TODO we require that the db contains both a head and a tail block - @@ -928,7 +933,7 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB, # The only allowed flag right now is strictVerification, as the others all # allow skipping some validation. updateFlags: updateFlags * { - strictVerification, enableTestFeatures, lowParticipation + strictVerification, experimental, enableTestFeatures, lowParticipation }, cfg: cfg, diff --git a/beacon_chain/extras.nim b/beacon_chain/extras.nim index 8e5f8c377..247c99285 100644 --- a/beacon_chain/extras.nim +++ b/beacon_chain/extras.nim @@ -36,6 +36,8 @@ type ## When process_slots() is being called as part of a state_transition(), ## the hash_tree_root() from the block will fill in the state.root so it ## should skip calculating that last state root. + experimental ##\ + ## Whether to enable extra features in development. enableTestFeatures ##\ ## Whether to enable extra features for testing. lowParticipation ##\ diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index b4099e3ba..e652dd03d 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -50,12 +50,13 @@ logScope: topics = "fork_choice" func init*( T: type ForkChoiceBackend, checkpoints: FinalityCheckpoints, - hasLowParticipation = false): T = - T(proto_array: ProtoArray.init(checkpoints, hasLowParticipation)) + experimental = false, hasLowParticipation = false): T = + T(proto_array: ProtoArray.init( + checkpoints, experimental, hasLowParticipation)) proc init*( T: type ForkChoice, epochRef: EpochRef, blck: BlockRef, - hasLowParticipation = false): T = + experimental = false, hasLowParticipation = false): T = ## Initialize a fork choice context for a finalized state - in the finalized ## state, the justified and finalized checkpoints are the same, so only one ## is used here @@ -68,8 +69,9 @@ proc init*( FinalityCheckpoints( justified: checkpoint, finalized: checkpoint), - hasLowParticipation), + experimental, hasLowParticipation), checkpoints: Checkpoints( + experimental: experimental, justified: BalanceCheckpoint( checkpoint: checkpoint, balances: epochRef.effective_balances), @@ -116,7 +118,7 @@ proc update_justified( epochRef = dag.getEpochRef(blck, epoch, false).valueOr: # Shouldn't happen for justified data unless out of sync with ChainDAG warn "Skipping justified checkpoint update, no EpochRef - report bug", - blck, epoch, best = self.best_justified.epoch, error + blck, epoch, error return justified = Checkpoint(root: blck.root, epoch: epochRef.epoch) @@ -144,10 +146,13 @@ proc update_checkpoints( ## Update checkpoints in store if necessary # Update justified checkpoint if checkpoints.justified.epoch > self.justified.checkpoint.epoch: - if checkpoints.justified.epoch > self.best_justified.epoch: - self.best_justified = checkpoints.justified + if not self.experimental: + if checkpoints.justified.epoch > self.best_justified.epoch: + self.best_justified = checkpoints.justified - if ? should_update_justified_checkpoint(self, dag, checkpoints.justified): + if ? should_update_justified_checkpoint(self, dag, checkpoints.justified): + ? self.update_justified(dag, checkpoints.justified) + else: ? self.update_justified(dag, checkpoints.justified) # Update finalized checkpoint @@ -155,8 +160,9 @@ proc update_checkpoints( trace "Updating finalized", store = self.finalized, state = checkpoints.finalized self.finalized = checkpoints.finalized - if checkpoints.justified != self.justified.checkpoint: - ? self.update_justified(dag, checkpoints.justified) + if not self.experimental: + if checkpoints.justified != self.justified.checkpoint: + ? self.update_justified(dag, checkpoints.justified) ok() @@ -185,18 +191,19 @@ proc on_tick( # Update store.justified_checkpoint if a better checkpoint on the # store.finalized_checkpoint chain - let - best_justified_epoch = self.checkpoints.best_justified.epoch - store_justified_epoch = self.checkpoints.justified.checkpoint.epoch - if best_justified_epoch > store_justified_epoch: + if not self.checkpoints.experimental: let - blck = dag.getBlockRef(self.checkpoints.best_justified.root).valueOr: - return err ForkChoiceError( - kind: fcJustifiedNodeUnknown, - blockRoot: self.checkpoints.best_justified.root) - finalized_ancestor = blck.atEpochStart(self.checkpoints.finalized.epoch) - if finalized_ancestor.blck.root == self.checkpoints.finalized.root: - self.checkpoints.update_justified(dag, blck, best_justified_epoch) + best_justified_epoch = self.checkpoints.best_justified.epoch + store_justified_epoch = self.checkpoints.justified.checkpoint.epoch + if best_justified_epoch > store_justified_epoch: + let + blck = dag.getBlockRef(self.checkpoints.best_justified.root).valueOr: + return err ForkChoiceError( + kind: fcJustifiedNodeUnknown, + blockRoot: self.checkpoints.best_justified.root) + finalized_ancestor = blck.atEpochStart(self.checkpoints.finalized.epoch) + if finalized_ancestor.blck.root == self.checkpoints.finalized.root: + self.checkpoints.update_justified(dag, blck, best_justified_epoch) # Pull-up chain tips from previous epoch for realized in self.backend.proto_array.realizePendingCheckpoints(): @@ -307,11 +314,11 @@ func process_equivocation*( # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.1/specs/phase0/fork-choice.md#on_block func process_block*(self: var ForkChoiceBackend, - block_root: Eth2Digest, + bid: BlockId, parent_root: Eth2Digest, checkpoints: FinalityCheckpoints, unrealized = none(FinalityCheckpoints)): FcResult[void] = - self.proto_array.onBlock(block_root, parent_root, checkpoints, unrealized) + self.proto_array.onBlock(bid, parent_root, checkpoints, unrealized) proc process_block*(self: var ForkChoice, dag: ChainDAGRef, @@ -358,14 +365,14 @@ proc process_block*(self: var ForkChoice, blck = shortLog(blckRef), checkpoints = epochRef.checkpoints, unrealized ? update_checkpoints(self.checkpoints, dag, unrealized) ? process_block( - self.backend, blckRef.root, blck.parent_root, unrealized) + self.backend, blckRef.bid, blck.parent_root, unrealized) else: ? process_block( - self.backend, blckRef.root, blck.parent_root, + self.backend, blckRef.bid, blck.parent_root, epochRef.checkpoints, some unrealized) # Realized in `on_tick` else: ? process_block( - self.backend, blckRef.root, blck.parent_root, epochRef.checkpoints) + self.backend, blckRef.bid, blck.parent_root, epochRef.checkpoints) ok() @@ -424,13 +431,16 @@ func get_safe_beacon_block_root*(self: ForkChoice): Eth2Digest = self.checkpoints.justified.checkpoint.root func prune*( - self: var ForkChoiceBackend, finalized_root: Eth2Digest + self: var ForkChoiceBackend, checkpoints: FinalityCheckpoints ): FcResult[void] = ## Prune blocks preceding the finalized root as they are now unneeded. - self.proto_array.prune(finalized_root) + self.proto_array.prune(checkpoints) func prune*(self: var ForkChoice): FcResult[void] = - self.backend.prune(self.checkpoints.finalized.root) + self.backend.prune( + FinalityCheckpoints( + justified: self.checkpoints.justified.checkpoint, + finalized: self.checkpoints.finalized)) func mark_root_invalid*(self: var ForkChoice, root: Eth2Digest) = try: diff --git a/beacon_chain/fork_choice/fork_choice_types.nim b/beacon_chain/fork_choice/fork_choice_types.nim index 2a31efeac..19f9a323b 100644 --- a/beacon_chain/fork_choice/fork_choice_types.nim +++ b/beacon_chain/fork_choice/fork_choice_types.nim @@ -88,6 +88,7 @@ type ## Subtracted from logical index to get the physical index ProtoArray* = object + experimental*: bool hasLowParticipation*: bool currentEpoch*: Epoch checkpoints*: FinalityCheckpoints @@ -98,7 +99,7 @@ type previousProposerBoostScore*: uint64 ProtoNode* = object - root*: Eth2Digest + bid*: BlockId parent*: Option[Index] checkpoints*: FinalityCheckpoints weight*: int64 @@ -111,6 +112,7 @@ type balances*: seq[Gwei] Checkpoints* = object + experimental*: bool time*: BeaconTime justified*: BalanceCheckpoint finalized*: Checkpoint diff --git a/beacon_chain/fork_choice/proto_array.nim b/beacon_chain/fork_choice/proto_array.nim index ffa54cee5..cd1b7e297 100644 --- a/beacon_chain/fork_choice/proto_array.nim +++ b/beacon_chain/fork_choice/proto_array.nim @@ -81,17 +81,21 @@ func maybeUpdateBestChildAndDescendant(self: var ProtoArray, parentIdx: Index, childIdx: Index): FcResult[void] -func nodeIsViableForHead(self: ProtoArray, node: ProtoNode): bool -func nodeLeadsToViableHead(self: ProtoArray, node: ProtoNode): FcResult[bool] +func nodeIsViableForHead( + self: ProtoArray, node: ProtoNode, nodeIdx: Index): bool +func nodeLeadsToViableHead( + self: ProtoArray, node: ProtoNode, nodeIdx: Index): FcResult[bool] # ProtoArray routines # ---------------------------------------------------------------------- func init*( T: type ProtoArray, checkpoints: FinalityCheckpoints, - hasLowParticipation: bool): T = + experimental, hasLowParticipation: bool): T = let node = ProtoNode( - root: checkpoints.finalized.root, + bid: BlockId( + slot: checkpoints.finalized.epoch.start_slot, + root: checkpoints.finalized.root), parent: none(int), checkpoints: checkpoints, weight: 0, @@ -99,10 +103,11 @@ func init*( bestChild: none(int), bestDescendant: none(int)) - T(hasLowParticipation: hasLowParticipation, + T(experimental: experimental, + hasLowParticipation: hasLowParticipation, checkpoints: checkpoints, nodes: ProtoNodes(buf: @[node], offset: 0), - indices: {node.root: 0}.toTable()) + indices: {node.bid.root: 0}.toTable()) iterator realizePendingCheckpoints*( self: var ProtoArray, resetTipTracking = true): FinalityCheckpoints = @@ -111,7 +116,7 @@ iterator realizePendingCheckpoints*( let physicalIdx = idx - self.nodes.offset if unrealized != self.nodes.buf[physicalIdx].checkpoints: trace "Pulling up chain tip", - blck = self.nodes.buf[physicalIdx].root, + blck = self.nodes.buf[physicalIdx].bid.root, checkpoints = self.nodes.buf[physicalIdx].checkpoints, unrealized self.nodes.buf[physicalIdx].checkpoints = unrealized @@ -173,7 +178,7 @@ func applyScoreChanges*(self: var ProtoArray, self.checkpoints = checkpoints # If previous epoch is justified, pull up all current tips to previous epoch - if self.hasLowParticipation and self.isPreviousEpochJustified: + if self.experimental and self.isPreviousEpochJustified: for realized in self.realizePendingCheckpoints(resetTipTracking = false): discard @@ -187,7 +192,7 @@ func applyScoreChanges*(self: var ProtoArray, # Iterate backwards through all the indices in `self.nodes` for nodePhysicalIdx in countdown(self.nodes.len - 1, 0): - if node.root.isZero: + if node.bid.root.isZero: continue var nodeDelta = deltas[nodePhysicalIdx] @@ -195,7 +200,7 @@ func applyScoreChanges*(self: var ProtoArray, # If we find the node for which the proposer boost was previously applied, # decrease the delta by the previous score amount. if (not self.previousProposerBoostRoot.isZero) and - self.previousProposerBoostRoot == node.root: + self.previousProposerBoostRoot == node.bid.root: if nodeDelta < 0 and nodeDelta - low(Delta) < self.previousProposerBoostScore.int64: return err ForkChoiceError( @@ -207,7 +212,7 @@ func applyScoreChanges*(self: var ProtoArray, # the delta by the new score amount. # # https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_latest_attesting_balance - if (not proposerBoostRoot.isZero) and proposerBoostRoot == node.root: + if (not proposerBoostRoot.isZero) and proposerBoostRoot == node.bid.root: proposerBoostScore = calculateProposerBoost(newBalances) if nodeDelta >= 0 and high(Delta) - nodeDelta < proposerBoostScore.int64: @@ -265,7 +270,7 @@ func applyScoreChanges*(self: var ProtoArray, self.previousProposerBoostScore = proposerBoostScore for nodePhysicalIdx in countdown(self.nodes.len - 1, 0): - if node.root.isZero: + if node.bid.root.isZero: continue if node.parent.isSome(): @@ -281,7 +286,7 @@ func applyScoreChanges*(self: var ProtoArray, ok() func onBlock*(self: var ProtoArray, - root: Eth2Digest, + bid: BlockId, parent: Eth2Digest, checkpoints: FinalityCheckpoints, unrealized = none(FinalityCheckpoints)): FcResult[void] = @@ -294,7 +299,7 @@ func onBlock*(self: var ProtoArray, # Note: if parent is an "Option" type, we can run out of stack space. # If the block is already known, ignore it - if root in self.indices: + if bid.root in self.indices: return ok() var parentIdx: Index @@ -303,13 +308,13 @@ func onBlock*(self: var ProtoArray, do: return err ForkChoiceError( kind: fcUnknownParent, - childRoot: root, + childRoot: bid.root, parentRoot: parent) let nodeLogicalIdx = self.nodes.offset + self.nodes.buf.len let node = ProtoNode( - root: root, + bid: bid, parent: some(parentIdx), checkpoints: checkpoints, weight: 0, @@ -317,11 +322,11 @@ func onBlock*(self: var ProtoArray, bestChild: none(int), bestDescendant: none(int)) - self.indices[node.root] = nodeLogicalIdx + self.indices[node.bid.root] = nodeLogicalIdx self.nodes.add node + self.currentEpochTips.del parentIdx if unrealized.isSome: - self.currentEpochTips.del parentIdx self.currentEpochTips[nodeLogicalIdx] = unrealized.get ? self.maybeUpdateBestChildAndDescendant(parentIdx, nodeLogicalIdx) @@ -359,34 +364,38 @@ func findHead*(self: var ProtoArray, index: bestDescendantIdx) # Perform a sanity check to ensure the node can be head - if not self.nodeIsViableForHead(bestNode.get()): + if not self.nodeIsViableForHead(bestNode.get(), bestDescendantIdx): return err ForkChoiceError( kind: fcInvalidBestNode, startRoot: justifiedRoot, fkChoiceCheckpoints: self.checkpoints, - headRoot: justifiedNode.get().root, + headRoot: justifiedNode.get().bid.root, headCheckpoints: justifiedNode.get().checkpoints) - head = bestNode.get().root + head = bestNode.get().bid.root ok() -func prune*(self: var ProtoArray, finalizedRoot: Eth2Digest): FcResult[void] = +func prune*( + self: var ProtoArray, + checkpoints: FinalityCheckpoints): FcResult[void] = ## Update the tree with new finalization information. ## The tree is pruned if and only if: - ## - The `finalizedRoot` and finalized epoch are different from current + ## - `checkpoints.finalized.root` and finalized epoch + ## are different from current ## ## Returns error if: ## - The finalized epoch is less than the current one - ## - The finalized epoch matches the current one but the finalized root is different + ## - The finalized epoch matches the current one but the f + ## inalized root is different ## - Internal error due to invalid indices in `self` var finalizedIdx: int - self.indices.withValue(finalizedRoot, value) do: + self.indices.withValue(checkpoints.finalized.root, value) do: finalizedIdx = value[] do: return err ForkChoiceError( kind: fcFinalizedNodeUnknown, - blockRoot: finalizedRoot) + blockRoot: checkpoints.finalized.root) if finalizedIdx == self.nodes.offset: # Nothing to do @@ -395,15 +404,14 @@ func prune*(self: var ProtoArray, finalizedRoot: Eth2Digest): FcResult[void] = if finalizedIdx < self.nodes.offset: return err ForkChoiceError( kind: fcPruningFromOutdatedFinalizedRoot, - finalizedRoot: finalizedRoot) + finalizedRoot: checkpoints.finalized.root) - trace "Pruning blocks from fork choice", - finalizedRoot = shortLog(finalizedRoot) + trace "Pruning blocks from fork choice", checkpoints let finalPhysicalIdx = finalizedIdx - self.nodes.offset for nodeIdx in 0 ..< finalPhysicalIdx: self.currentEpochTips.del nodeIdx - self.indices.del(self.nodes.buf[nodeIdx].root) + self.indices.del(self.nodes.buf[nodeIdx].bid.root) # Drop all nodes prior to finalization. # This is done in-place with `moveMem` to avoid costly reallocations. @@ -445,7 +453,8 @@ func maybeUpdateBestChildAndDescendant(self: var ProtoArray, kind: fcInvalidNodeIndex, index: parentIdx) - let childLeadsToViableHead = ? self.nodeLeadsToViableHead(child.get()) + let childLeadsToViableHead = + ? self.nodeLeadsToViableHead(child.get(), childIdx) let # Aliases to the 3 possible (bestChild, bestDescendant) tuples changeToNone = (none(Index), none(Index)) @@ -477,7 +486,7 @@ func maybeUpdateBestChildAndDescendant(self: var ProtoArray, index: bestChildIdx) let bestChildLeadsToViableHead = - ? self.nodeLeadsToViableHead(bestChild.get()) + ? self.nodeLeadsToViableHead(bestChild.get(), bestChildIdx) if childLeadsToViableHead and not bestChildLeadsToViableHead: # The child leads to a viable head, but the current best-child doesn't @@ -487,7 +496,7 @@ func maybeUpdateBestChildAndDescendant(self: var ProtoArray, noChange elif child.get().weight == bestChild.get().weight: # Tie-breaker of equal weights by root - if child.get().root.tiebreak(bestChild.get().root): + if child.get().bid.root.tiebreak(bestChild.get().bid.root): changeToChild else: noChange @@ -511,7 +520,8 @@ func maybeUpdateBestChildAndDescendant(self: var ProtoArray, ok() -func nodeLeadsToViableHead(self: ProtoArray, node: ProtoNode): FcResult[bool] = +func nodeLeadsToViableHead( + self: ProtoArray, node: ProtoNode, nodeIdx: Index): FcResult[bool] = ## Indicates if the node itself or its best-descendant are viable ## for blockchain head let bestDescendantIsViableForHead = block: @@ -522,13 +532,14 @@ func nodeLeadsToViableHead(self: ProtoArray, node: ProtoNode): FcResult[bool] = return err ForkChoiceError( kind: fcInvalidBestDescendant, index: bestDescendantIdx) - self.nodeIsViableForHead(bestDescendant.get()) + self.nodeIsViableForHead(bestDescendant.get(), bestDescendantIdx) else: false - ok(bestDescendantIsViableForHead or self.nodeIsViableForHead(node)) + ok(bestDescendantIsViableForHead or self.nodeIsViableForHead(node, nodeIdx)) -func nodeIsViableForHead(self: ProtoArray, node: ProtoNode): bool = +func nodeIsViableForHead( + self: ProtoArray, node: ProtoNode, nodeIdx: Index): bool = ## This is the equivalent of `filter_block_tree` function in eth2 spec ## https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.1/specs/phase0/fork-choice.md#filter_block_tree @@ -545,6 +556,35 @@ func nodeIsViableForHead(self: ProtoArray, node: ProtoNode): bool = node.checkpoints.finalized == self.checkpoints.finalized or self.checkpoints.finalized.epoch == GENESIS_EPOCH + if self.experimental: + var correctJustified = + self.checkpoints.justified.epoch == GENESIS_EPOCH or + node.checkpoints.justified.epoch == self.checkpoints.justified.epoch + if not correctJustified and self.isPreviousEpochJustified and + node.bid.slot.epoch == self.currentEpoch: + let unrealized = + self.currentEpochTips.getOrDefault(nodeIdx, node.checkpoints) + correctJustified = + unrealized.justified.epoch >= self.checkpoints.justified.epoch and + node.checkpoints.justified.epoch + 2 >= self.currentEpoch + return + if not correctJustified: + false + elif self.checkpoints.finalized.epoch == GENESIS_EPOCH: + true + else: + let finalizedSlot = self.checkpoints.finalized.epoch.start_slot + var ancestor = some node + while ancestor.isSome and ancestor.unsafeGet.bid.slot > finalizedSlot: + if ancestor.unsafeGet.parent.isSome: + ancestor = self.nodes[ancestor.unsafeGet.parent.unsafeGet] + else: + ancestor.reset() + if ancestor.isSome: + ancestor.unsafeGet.bid.root == self.checkpoints.finalized.root + else: + false + ## Any node that has a different finalized or justified epoch ## should not be viable for the head. ( @@ -582,7 +622,7 @@ func propagateInvalidity*( # Helpers to dump internal state type ProtoArrayItem* = object - root*: Eth2Digest + bid*: BlockId parent*: Eth2Digest checkpoints*: FinalityCheckpoints unrealized*: Option[FinalityCheckpoints] @@ -597,13 +637,13 @@ func root(self: ProtoNodes, logicalIdx: Option[Index]): Eth2Digest = let node = self[logicalIdx.unsafeGet] if node.isNone: return ZERO_HASH - node.unsafeGet.root + node.unsafeGet.bid.root iterator items*(self: ProtoArray): ProtoArrayItem = ## Iterate over all nodes known by fork choice. doAssert self.indices.len == self.nodes.len for nodePhysicalIdx, node in self.nodes.buf: - if node.root.isZero: + if node.bid.root.isZero: continue let unrealized = block: @@ -614,7 +654,7 @@ iterator items*(self: ProtoArray): ProtoArrayItem = none(FinalityCheckpoints) yield ProtoArrayItem( - root: node.root, + bid: node.bid, parent: self.nodes.root(node.parent), checkpoints: node.checkpoints, unrealized: unrealized, diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index dbadf64cd..05ebb7654 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -142,8 +142,7 @@ proc loadChainDag( db: BeaconChainDB, eventBus: EventBus, validatorMonitor: ref ValidatorMonitor, - networkGenesisValidatorsRoot: Opt[Eth2Digest], - shouldEnableTestFeatures: bool): ChainDAGRef = + networkGenesisValidatorsRoot: Opt[Eth2Digest]): ChainDAGRef = info "Loading block DAG from database", path = config.databaseDir var dag: ChainDAGRef @@ -170,10 +169,12 @@ proc loadChainDag( jsonVersion: contextFork, sszContext: dag.forkDigests[].atStateFork(contextFork))) + var extraFlags = {enableTestFeatures} + if config.deploymentPhase <= DeploymentPhase.Testnet: + extraFlags.incl experimental + if config.deploymentPhase <= DeploymentPhase.Devnet: + extraFlags.incl lowParticipation let - extraFlags = - if shouldEnableTestFeatures: {enableTestFeatures, lowParticipation} - else: {enableTestFeatures} chainDagFlags = if config.strictVerification: {strictVerification} else: {} @@ -548,8 +549,7 @@ proc init*(T: type BeaconNode, dag = loadChainDag( config, cfg, db, eventBus, - validatorMonitor, networkGenesisValidatorsRoot, - config.deploymentPhase <= DeploymentPhase.Testnet) + validatorMonitor, networkGenesisValidatorsRoot) genesisTime = getStateField(dag.headState, genesis_time) beaconClock = BeaconClock.init(genesisTime) getBeaconTime = beaconClock.getBeaconTimeFn() diff --git a/beacon_chain/rpc/rest_debug_api.nim b/beacon_chain/rpc/rest_debug_api.nim index 75544db51..08e1dea8d 100644 --- a/beacon_chain/rpc/rest_debug_api.nim +++ b/beacon_chain/rpc/rest_debug_api.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2021-2022 Status Research & Development GmbH +# Copyright (c) 2021-2023 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -82,7 +82,7 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = ) # https://github.com/ethereum/beacon-APIs/pull/232 - if node.config.debugForkChoice: + if node.config.debugForkChoice or experimental in node.dag.updateFlags: router.api(MethodGet, "/eth/v1/debug/fork_choice") do () -> RestApiResponse: type @@ -109,17 +109,6 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = var responses: seq[ForkChoiceResponse] for item in node.attestationPool[].forkChoice.backend.proto_array: let - bid = node.dag.getBlockId(item.root) - slot = - if bid.isOk: - bid.unsafeGet.slot - else: - FAR_FUTURE_SLOT - executionPayloadRoot = - if bid.isOk: - node.dag.loadExecutionBlockRoot(bid.unsafeGet) - else: - ZERO_HASH unrealized = item.unrealized.get(item.checkpoints) u_justified_checkpoint = if unrealized.justified != item.checkpoints.justified: @@ -133,14 +122,14 @@ proc installDebugApiHandlers*(router: var RestRouter, node: BeaconNode) = none(Checkpoint) responses.add ForkChoiceResponse( - slot: slot, - block_root: item.root, + slot: item.bid.slot, + block_root: item.bid.root, parent_root: item.parent, justified_epoch: item.checkpoints.justified.epoch, finalized_epoch: item.checkpoints.finalized.epoch, weight: cast[uint64](item.weight), - execution_optimistic: node.dag.is_optimistic(item.root), - execution_payload_root: executionPayloadRoot, + execution_optimistic: node.dag.is_optimistic(item.bid.root), + execution_payload_root: node.dag.loadExecutionBlockRoot(item.bid), extra_data: some ForkChoiceResponseExtraData( justified_root: item.checkpoints.justified.root, finalized_root: item.checkpoints.finalized.root, diff --git a/tests/fork_choice/interpreter.nim b/tests/fork_choice/interpreter.nim index c0502c58b..3a6a8dc67 100644 --- a/tests/fork_choice/interpreter.nim +++ b/tests/fork_choice/interpreter.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2018-2022 Status Research & Development GmbH +# Copyright (c) 2018-2023 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -46,7 +46,7 @@ type justified_state_balances*: seq[Gwei] expected_head*: Eth2Digest of ProcessBlock: - root*: Eth2Digest + bid*: BlockId parent_root*: Eth2Digest blk_checkpoints*: FinalityCheckpoints of ProcessAttestation: @@ -54,7 +54,7 @@ type block_root*: Eth2Digest target_epoch*: Epoch of Prune: # ProtoArray specific - finalized_root*: Eth2Digest + prune_checkpoints*: FinalityCheckpoints expected_len*: int func apply(ctx: var ForkChoiceBackend, id: int, op: Operation) = @@ -79,11 +79,11 @@ func apply(ctx: var ForkChoiceBackend, id: int, op: Operation) = debugEcho &" Detected an expected invalid head from justified checkpoint {op.checkpoints.justified}, finalized checkpoint {op.checkpoints.finalized}" of ProcessBlock: let r = ctx.process_block( - block_root = op.root, + bid = op.bid, parent_root = op.parent_root, checkpoints = op.blk_checkpoints) doAssert r.isOk(), &"process_block (op #{id}) returned an error: {r.error}" - debugEcho " Processed block 0x", op.root, " with parent 0x", op.parent_root, " and justified checkpoint ", op.blk_checkpoints.justified + debugEcho " Processed block 0x", op.bid.root, " with parent 0x", op.parent_root, " and justified checkpoint ", op.blk_checkpoints.justified of ProcessAttestation: ctx.process_attestation( validator_index = op.validator_index, @@ -91,11 +91,11 @@ func apply(ctx: var ForkChoiceBackend, id: int, op: Operation) = target_epoch = op.target_epoch) debugEcho " Processed att target 0x", op.block_root, " from validator ", op.validator_index, " for epoch ", op.target_epoch of Prune: - let r = ctx.prune(op.finalized_root) + let r = ctx.prune(op.prune_checkpoints) doAssert r.isOk(), &"prune (op #{id}) returned an error: {r.error}" doAssert ctx.proto_array.nodes.len == op.expected_len, &"prune (op #{id}): the resulting length ({ctx.proto_array.nodes.len}) was not expected ({op.expected_len})" - debugEcho " Maybe_pruned block preceding finalized block 0x", op.finalized_root + debugEcho " Maybe_pruned block preceding finalized block 0x", op.prune_checkpoints.finalized.root func run*(ctx: var ForkChoiceBackend, ops: seq[Operation]) = ## Apply a sequence of fork-choice operations on a store diff --git a/tests/fork_choice/scenarios/ffg_01.nim b/tests/fork_choice/scenarios/ffg_01.nim index 56849591c..02fa4fd04 100644 --- a/tests/fork_choice/scenarios/ffg_01.nim +++ b/tests/fork_choice/scenarios/ffg_01.nim @@ -41,7 +41,9 @@ func setup_finality_01(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati # 3 <- just: 2, fin: 1 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(1), + bid: BlockId( + slot: Epoch(1).start_slot, + root: fakeHash(1)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(0)), @@ -49,7 +51,9 @@ func setup_finality_01(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(2), + bid: BlockId( + slot: Epoch(2).start_slot, + root: fakeHash(2)), parent_root: fakeHash(1), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(1), epoch: Epoch(1)), @@ -57,7 +61,9 @@ func setup_finality_01(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(3), + bid: BlockId( + slot: Epoch(3).start_slot, + root: fakeHash(3)), parent_root: fakeHash(2), blk_Checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(2), epoch: Epoch(2)), diff --git a/tests/fork_choice/scenarios/ffg_02.nim b/tests/fork_choice/scenarios/ffg_02.nim index 59ac64507..e842ab64d 100644 --- a/tests/fork_choice/scenarios/ffg_02.nim +++ b/tests/fork_choice/scenarios/ffg_02.nim @@ -47,7 +47,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati # Left branch result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(1), + bid: BlockId( + slot: Slot(1), + root: fakeHash(1)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(0)), @@ -55,7 +57,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(3), + bid: BlockId( + slot: Epoch(2).start_slot, + root: fakeHash(3)), parent_root: fakeHash(1), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(1), epoch: Epoch(1)), @@ -63,7 +67,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(5), + bid: BlockId( + slot: Epoch(2).start_slot + 2, + root: fakeHash(5)), parent_root: fakeHash(3), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(1), epoch: Epoch(1)), @@ -71,7 +77,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(7), + bid: BlockId( + slot: Epoch(2).start_slot + 4, + root: fakeHash(7)), parent_root: fakeHash(5), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(1), epoch: Epoch(1)), @@ -79,7 +87,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(9), + bid: BlockId( + slot: Epoch(3).start_slot, + root: fakeHash(9)), parent_root: fakeHash(7), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(3), epoch: Epoch(2)), @@ -102,7 +112,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati # Right branch result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(2), + bid: BlockId( + slot: Slot(2), + root: fakeHash(2)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(0)), @@ -110,7 +122,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(4), + bid: BlockId( + slot: Epoch(1).start_slot + 1, + root: fakeHash(4)), parent_root: fakeHash(2), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(0)), @@ -118,7 +132,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(6), + bid: BlockId( + slot: Epoch(2).start_slot + 3, + root: fakeHash(6)), parent_root: fakeHash(4), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(0)), @@ -126,7 +142,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(8), + bid: BlockId( + slot: Epoch(3).start_slot + 1, + root: fakeHash(8)), parent_root: fakeHash(6), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(2), epoch: Epoch(1)), @@ -134,7 +152,9 @@ func setup_finality_02(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operati result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(10), + bid: BlockId( + slot: Epoch(5).start_slot + 1, + root: fakeHash(10)), parent_root: fakeHash(8), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(4), epoch: Epoch(2)), diff --git a/tests/fork_choice/scenarios/no_votes.nim b/tests/fork_choice/scenarios/no_votes.nim index 78d7d7db3..71c55e3a2 100644 --- a/tests/fork_choice/scenarios/no_votes.nim +++ b/tests/fork_choice/scenarios/no_votes.nim @@ -38,7 +38,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 2 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(2), + bid: BlockId( + slot: Epoch(3).start_slot + 2, + root: fakeHash(2)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -64,7 +66,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 2 1 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(1), + bid: BlockId( + slot: Epoch(3).start_slot + 1, + root: fakeHash(1)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -92,7 +96,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 3 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(3), + bid: BlockId( + slot: Epoch(3).start_slot + 3, + root: fakeHash(3)), parent_root: fakeHash(1), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -122,7 +128,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 4 3 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(4), + bid: BlockId( + slot: Epoch(3).start_slot + 4, + root: fakeHash(4)), parent_root: fakeHash(2), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -154,7 +162,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 5 <- justified epoch = 2 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(5), + bid: BlockId( + slot: Epoch(4).start_slot, + root: fakeHash(5)), parent_root: fakeHash(4), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -221,7 +231,9 @@ func setup_no_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation] # 6 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(6), + bid: BlockId( + slot: Epoch(4).start_slot + 1, + root: fakeHash(6)), parent_root: fakeHash(5), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), diff --git a/tests/fork_choice/scenarios/votes.nim b/tests/fork_choice/scenarios/votes.nim index 332d95aa5..a655be2fb 100644 --- a/tests/fork_choice/scenarios/votes.nim +++ b/tests/fork_choice/scenarios/votes.nim @@ -38,7 +38,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 2 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(2), + bid: BlockId( + slot: Epoch(1).start_slot + 2, + root: fakeHash(2)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -64,7 +66,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 2 1 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(1), + bid: BlockId( + slot: Epoch(1).start_slot + 1, + root: fakeHash(1)), parent_root: GenesisRoot, blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -140,7 +144,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 3 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(3), + bid: BlockId( + slot: Epoch(1).start_slot + 3, + root: fakeHash(3)), parent_root: fakeHash(1), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -226,7 +232,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 4 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(4), + bid: BlockId( + slot: Epoch(1).start_slot + 4, + root: fakeHash(4)), parent_root: fakeHash(3), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -262,7 +270,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 5 <- justified epoch = 2 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(5), + bid: BlockId( + slot: Epoch(2).start_slot, + root: fakeHash(5)), parent_root: fakeHash(4), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -300,7 +310,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 5 6 <- justified epoch = 0 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(6), + bid: BlockId( + slot: Epoch(2).start_slot + 1, + root: fakeHash(6)), parent_root: fakeHash(4), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: GenesisRoot, epoch: Epoch(1)), @@ -349,7 +361,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 9 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(7), + bid: BlockId( + slot: Epoch(2).start_slot + 2, + root: fakeHash(7)), parent_root: fakeHash(5), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -357,7 +371,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(8), + bid: BlockId( + slot: Epoch(2).start_slot + 3, + root: fakeHash(8)), parent_root: fakeHash(7), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -366,7 +382,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # Finalizes 5 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(9), + bid: BlockId( + slot: Epoch(2).start_slot + 4, + root: fakeHash(9)), parent_root: fakeHash(8), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -481,7 +499,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 9 10 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(10), + bid: BlockId( + slot: Epoch(3).start_slot, + root: fakeHash(10)), parent_root: fakeHash(8), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), @@ -626,7 +646,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # - 6 is a discarded chain result.ops.add Operation( kind: Prune, - finalized_root: fakeHash(5), + prune_checkpoints: FinalityCheckpoints( + justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)), + finalized: Checkpoint(root: fakeHash(5), epoch: Epoch(2))), expected_len: 6) # Prune shouldn't have changed the head @@ -651,7 +673,9 @@ func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] = # 11 result.ops.add Operation( kind: ProcessBlock, - root: fakeHash(11), + bid: BlockId( + slot: Epoch(3).start_slot + 1, + root: fakeHash(11)), parent_root: fakeHash(9), blk_checkpoints: FinalityCheckpoints( justified: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),