Fork choice update for HF1

This commit is contained in:
Mamy André-Ratsimbazafy 2021-02-16 19:53:07 +01:00 committed by zah
parent 3d25f0db01
commit 597374605a
3 changed files with 204 additions and 220 deletions

View File

@ -97,7 +97,7 @@ proc compute_slots_since_epoch_start(slot: Slot): uint64 =
proc on_tick(self: var Checkpoints, dag: ChainDAGRef, time: Slot): FcResult[void] = proc on_tick(self: var Checkpoints, dag: ChainDAGRef, time: Slot): FcResult[void] =
if self.time > time: if self.time > time:
return err(ForkChoiceError(kind: fcInconsistentTick)) return err ForkChoiceError(kind: fcInconsistentTick)
let newEpoch = self.time.epoch() != time.epoch() let newEpoch = self.time.epoch() != time.epoch()
self.time = time self.time = time
@ -106,8 +106,9 @@ proc on_tick(self: var Checkpoints, dag: ChainDAGRef, time: Slot): FcResult[void
self.best_justified.epoch > self.justified.epoch: self.best_justified.epoch > self.justified.epoch:
let blck = dag.getRef(self.best_justified.root) let blck = dag.getRef(self.best_justified.root)
if blck.isNil: if blck.isNil:
return err(ForkChoiceError( return err ForkChoiceError(
kind: fcJustifiedNodeUnknown, block_root: self.best_justified.root)) kind: fcJustifiedNodeUnknown,
blockRoot: self.best_justified.root)
let epochRef = dag.getEpochRef(blck, self.best_justified.epoch) let epochRef = dag.getEpochRef(blck, self.best_justified.epoch)
self.justified = BalanceCheckpoint( self.justified = BalanceCheckpoint(
@ -117,6 +118,7 @@ proc on_tick(self: var Checkpoints, dag: ChainDAGRef, time: Slot): FcResult[void
ok() ok()
proc process_attestation_queue(self: var ForkChoice) {.gcsafe.} proc process_attestation_queue(self: var ForkChoice) {.gcsafe.}
proc update_time(self: var ForkChoice, dag: ChainDAGRef, time: Slot): FcResult[void] = proc update_time(self: var ForkChoice, dag: ChainDAGRef, time: Slot): FcResult[void] =
if time > self.checkpoints.time: if time > self.checkpoints.time:
while time > self.checkpoints.time: while time > self.checkpoints.time:
@ -209,8 +211,9 @@ proc should_update_justified_checkpoint(
justified_blck = dag.getRef(new_justified_checkpoint.root) justified_blck = dag.getRef(new_justified_checkpoint.root)
if justified_blck.isNil: if justified_blck.isNil:
return err(ForkChoiceError( return err ForkChoiceError(
kind: fcJustifiedNodeUnknown, block_root: new_justified_checkpoint.root)) kind: fcJustifiedNodeUnknown,
blockRoot: new_justified_checkpoint.root)
let justified_ancestor = justified_blck.atSlot(justified_slot) let justified_ancestor = justified_blck.atSlot(justified_slot)
@ -275,7 +278,7 @@ proc process_block*(self: var ForkChoiceBackend,
parent_root: Eth2Digest, parent_root: Eth2Digest,
justified_epoch: Epoch, justified_epoch: Epoch,
finalized_epoch: Epoch): FcResult[void] = finalized_epoch: Epoch): FcResult[void] =
self.proto_array.on_block( self.proto_array.onBlock(
block_root, parent_root, justified_epoch, finalized_epoch) block_root, parent_root, justified_epoch, finalized_epoch)
proc process_block*(self: var ForkChoice, proc process_block*(self: var ForkChoice,
@ -336,7 +339,7 @@ proc find_head*(
) )
# Apply score changes # Apply score changes
? self.proto_array.apply_score_changes( ? self.proto_array.applyScoreChanges(
deltas, justified_epoch, finalized_epoch deltas, justified_epoch, finalized_epoch
) )
@ -344,7 +347,7 @@ proc find_head*(
# Find the best block # Find the best block
var new_head{.noInit.}: Eth2Digest var new_head{.noInit.}: Eth2Digest
? self.proto_array.find_head(new_head, justified_root) ? self.proto_array.findHead(new_head, justified_root)
{.noSideEffect.}: {.noSideEffect.}:
trace "Fork choice requested", trace "Fork choice requested",
@ -425,8 +428,7 @@ func compute_deltas(
if index >= deltas.len: if index >= deltas.len:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidNodeDelta, kind: fcInvalidNodeDelta,
index: index index: index)
)
deltas[index] -= Delta old_balance deltas[index] -= Delta old_balance
# Note that delta can be negative # Note that delta can be negative
# TODO: is int64 big enough? # TODO: is int64 big enough?
@ -436,8 +438,7 @@ func compute_deltas(
if index >= deltas.len: if index >= deltas.len:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidNodeDelta, kind: fcInvalidNodeDelta,
index: index index: index)
)
deltas[index] += Delta new_balance deltas[index] += Delta new_balance
# Note that delta can be negative # Note that delta can be negative
# TODO: is int64 big enough? # TODO: is int64 big enough?

View File

@ -76,7 +76,7 @@ type
case kind*: fcKind case kind*: fcKind
of fcFinalizedNodeUnknown, of fcFinalizedNodeUnknown,
fcJustifiedNodeUnknown: fcJustifiedNodeUnknown:
block_root*: Eth2Digest blockRoot*: Eth2Digest
of fcInvalidFinalizedRootChange, of fcInvalidFinalizedRootChange,
fcInconsistentTick: fcInconsistentTick:
discard discard
@ -95,18 +95,18 @@ type
deltasLen*: int deltasLen*: int
indicesLen*: int indicesLen*: int
of fcRevertedFinalizedEpoch: of fcRevertedFinalizedEpoch:
current_finalized_epoch*: Epoch currentFinalizedEpoch*: Epoch
new_finalized_epoch*: Epoch new_finalized_epoch*: Epoch
of fcInvalidBestNode: of fcInvalidBestNode:
start_root*: Eth2Digest startRoot*: Eth2Digest
justified_epoch*: Epoch justifiedEpoch*: Epoch
finalized_epoch*: Epoch finalizedEpoch*: Epoch
head_root*: Eth2Digest headRoot*: Eth2Digest
head_justified_epoch*: Epoch headJustifiedEpoch*: Epoch
head_finalized_epoch*: Epoch headFinalizedEpoch*: Epoch
of fcUnknownParent: of fcUnknownParent:
child_root*: Eth2Digest childRoot*: Eth2Digest
parent_root*: Eth2Digest parentRoot*: Eth2Digest
of fcPruningFromOutdatedFinalizedRoot: of fcPruningFromOutdatedFinalizedRoot:
finalizedRoot*: Eth2Digest finalizedRoot*: Eth2Digest
@ -119,19 +119,19 @@ type
## to get the physical index ## to get the physical index
ProtoArray* = object ProtoArray* = object
justified_epoch*: Epoch justifiedEpoch*: Epoch
finalized_epoch*: Epoch finalizedEpoch*: Epoch
nodes*: Protonodes nodes*: Protonodes
indices*: Table[Eth2Digest, Index] indices*: Table[Eth2Digest, Index]
ProtoNode* = object ProtoNode* = object
root*: Eth2Digest root*: Eth2Digest
parent*: Option[Index] parent*: Option[Index]
justified_epoch*: Epoch justifiedEpoch*: Epoch
finalized_epoch*: Epoch finalizedEpoch*: Epoch
weight*: int64 weight*: int64
best_child*: Option[Index] bestChild*: Option[Index]
best_descendant*: Option[Index] bestDescendant*: Option[Index]
BalanceCheckpoint* = object BalanceCheckpoint* = object
blck*: BlockRef blck*: BlockRef

View File

@ -73,41 +73,41 @@ func add(nodes: var ProtoNodes, node: ProtoNode) =
# Forward declarations # Forward declarations
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
func maybe_update_best_child_and_descendant( func maybeUpdateBestChildAndDescendant(self: var ProtoArray,
self: var ProtoArray, parent_index: Index, child_index: Index): FcResult[void] parentIdx: Index,
func node_is_viable_for_head(self: ProtoArray, node: ProtoNode): bool childIdx: Index): FcResult[void]
func node_leads_to_viable_head(self: ProtoArray, node: ProtoNode): FcResult[bool]
func nodeIsViableForHead(self: ProtoArray, node: ProtoNode): bool
func nodeLeadsToViableHead(self: ProtoArray, node: ProtoNode): FcResult[bool]
# ProtoArray routines # ProtoArray routines
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
func init*(T: type ProtoArray, func init*(T: type ProtoArray,
justified_epoch: Epoch, justifiedEpoch: Epoch,
finalized_root: Eth2Digest, finalizedRoot: Eth2Digest,
finalized_epoch: Epoch): T = finalizedEpoch: Epoch): T =
let node = ProtoNode( let node = ProtoNode(
root: finalized_root, root: finalizedRoot,
parent: none(int), parent: none(int),
justified_epoch: justified_epoch, justifiedEpoch: justifiedEpoch,
finalized_epoch: finalized_epoch, finalizedEpoch: finalizedEpoch,
weight: 0, weight: 0,
best_child: none(int), bestChild: none(int),
best_descendant: none(int) bestDescendant: none(int)
) )
T( T(
justified_epoch: justified_epoch, justifiedEpoch: justifiedEpoch,
finalized_epoch: finalized_epoch, finalizedEpoch: finalizedEpoch,
nodes: ProtoNodes(buf: @[node], offset: 0), nodes: ProtoNodes(buf: @[node], offset: 0),
indices: {node.root: 0}.toTable() indices: {node.root: 0}.toTable()
) )
func apply_score_changes*( func applyScoreChanges*(self: var ProtoArray,
self: var ProtoArray, deltas: var openArray[Delta],
deltas: var openArray[Delta], justifiedEpoch: Epoch,
justified_epoch: Epoch, finalizedEpoch: Epoch): FcResult[void] =
finalized_epoch: Epoch
): FcResult[void] =
## Iterate backwards through the array, touching all nodes and their parents ## Iterate backwards through the array, touching all nodes and their parents
## and potentially the best-child of each parent. ## and potentially the best-child of each parent.
## ##
@ -124,41 +124,40 @@ func apply_score_changes*(
doAssert self.indices.len == self.nodes.len # By construction doAssert self.indices.len == self.nodes.len # By construction
if deltas.len != self.indices.len: if deltas.len != self.indices.len:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidDeltaLen, kind: fcInvalidDeltaLen,
deltasLen: deltas.len, deltasLen: deltas.len,
indicesLen: self.indices.len indicesLen: self.indices.len)
)
self.justified_epoch = justified_epoch self.justifiedEpoch = justifiedEpoch
self.finalized_epoch = finalized_epoch self.finalizedEpoch = finalizedEpoch
## Alias
# This cannot raise the IndexError exception, how to tell compiler?
template node: untyped {.dirty.} =
self.nodes.buf[nodePhysicalIdx]
# Iterate backwards through all the indices in `self.nodes` # Iterate backwards through all the indices in `self.nodes`
for node_physical_index in countdown(self.nodes.len - 1, 0): for nodePhysicalIdx in countdown(self.nodes.len - 1, 0):
template node: untyped {.dirty.}= self.nodes.buf[node_physical_index]
## Alias
# This cannot raise the IndexError exception, how to tell compiler?
if node.root == default(Eth2Digest): if node.root == default(Eth2Digest):
continue continue
let node_delta = deltas[node_physical_index] let nodeDelta = deltas[nodePhysicalIdx]
# Apply the delta to the node # Apply the delta to the node
# We fail fast if underflow, which shouldn't happen. # We fail fast if underflow, which shouldn't happen.
# Note that delta can be negative but weight cannot # Note that delta can be negative but weight cannot
let weight = node.weight + node_delta let weight = node.weight + nodeDelta
if weight < 0: if weight < 0:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcDeltaUnderflow, kind: fcDeltaUnderflow,
index: node_physical_index index: nodePhysicalIdx)
)
node.weight = weight node.weight = weight
# If the node has a parent, try to update its best-child and best-descendant # If the node has a parent, try to update its best-child and best-descendant
if node.parent.isSome(): if node.parent.isSome():
let parent_logical_index = node.parent.unsafeGet() let parentLogicalIdx = node.parent.unsafeGet()
let parent_physical_index = parent_logical_index - self.nodes.offset let parentPhysicalIdx = parentLogicalIdx - self.nodes.offset
if parent_physical_index < 0: if parentPhysicalIdx < 0:
# Orphan, for example # Orphan, for example
# 0 # 0
# / \ # / \
@ -180,27 +179,35 @@ func apply_score_changes*(
# that will be pruned next. # that will be pruned next.
continue continue
if parent_physical_index >= deltas.len: if parentPhysicalIdx >= deltas.len:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidParentDelta, kind: fcInvalidParentDelta,
index: parent_physical_index index: parentPhysicalIdx)
)
# Back-propagate the nodes delta to its parent. # Back-propagate the nodes delta to its parent.
deltas[parent_physical_index] += node_delta deltas[parentPhysicalIdx] += nodeDelta
let node_logical_index = node_physical_index + self.nodes.offset for nodePhysicalIdx in countdown(self.nodes.len - 1, 0):
? self.maybe_update_best_child_and_descendant(parent_logical_index, node_logical_index) if node.root == default(Eth2Digest):
continue
return ok() if node.parent.isSome():
let parentLogicalIdx = node.parent.unsafeGet()
let parentPhysicalIdx = parentLogicalIdx - self.nodes.offset
if parentPhysicalIdx < 0:
# Orphan
continue
func on_block*( let nodeLogicalIdx = nodePhysicalIdx + self.nodes.offset
self: var ProtoArray, ? self.maybeUpdateBestChildAndDescendant(parentLogicalIdx, nodeLogicalIdx)
root: Eth2Digest,
parent: Eth2Digest, ok()
justified_epoch: Epoch,
finalized_epoch: Epoch func onBlock*(self: var ProtoArray,
): FcResult[void] = root: Eth2Digest,
parent: Eth2Digest,
justifiedEpoch: Epoch,
finalizedEpoch: Epoch): FcResult[void] =
## Register a block with the fork choice ## Register a block with the fork choice
## A block `hasParentInForkChoice` may be false ## A block `hasParentInForkChoice` may be false
## on fork choice initialization: ## on fork choice initialization:
@ -213,145 +220,130 @@ func on_block*(
if root in self.indices: if root in self.indices:
return ok() return ok()
var parent_index: Index var parentIdx: Index
self.indices.withValue(parent, index) do: self.indices.withValue(parent, index) do:
parent_index = index[] parentIdx = index[]
do: do:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcUnknownParent, kind: fcUnknownParent,
child_root: root, childRoot: root,
parent_root: parent parentRoot: parent)
)
let node_logical_index = self.nodes.offset + self.nodes.buf.len let nodeLogicalIdx = self.nodes.offset + self.nodes.buf.len
let node = ProtoNode( let node = ProtoNode(
root: root, root: root,
parent: some(parent_index), parent: some(parentIdx),
justified_epoch: justified_epoch, justifiedEpoch: justifiedEpoch,
finalized_epoch: finalized_epoch, finalizedEpoch: finalizedEpoch,
weight: 0, weight: 0,
best_child: none(int), bestChild: none(int),
best_descendant: none(int) bestDescendant: none(int)
) )
self.indices[node.root] = node_logical_index self.indices[node.root] = nodeLogicalIdx
self.nodes.add node self.nodes.add node
? self.maybe_update_best_child_and_descendant(parent_index, node_logical_index) ? self.maybeUpdateBestChildAndDescendant(parentIdx, nodeLogicalIdx)
return ok() ok()
func find_head*( func findHead*(self: var ProtoArray,
self: var ProtoArray, head: var Eth2Digest,
head: var Eth2Digest, justifiedRoot: Eth2Digest): FcResult[void] =
justified_root: Eth2Digest
): FcResult[void] =
## Follows the best-descendant links to find the best-block (i.e. head-block) ## Follows the best-descendant links to find the best-block (i.e. head-block)
## ##
## ⚠️ Warning ## ⚠️ Warning
## The result may not be accurate if `on_new_block` ## The result may not be accurate if `onBlock` is not followed by
## is not followed by `apply_score_changes` as `on_new_block` does not ## `applyScoreChanges` as `onBlock` does not update the whole tree.
## update the whole tree.
var justified_index: Index var justifiedIdx: Index
self.indices.withValue(justified_root, value) do: self.indices.withValue(justifiedRoot, value) do:
justified_index = value[] justifiedIdx = value[]
do: do:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcJustifiedNodeUnknown, kind: fcJustifiedNodeUnknown,
block_root: justified_root blockRoot: justifiedRoot)
)
let justified_node = self.nodes[justified_index] let justifiedNode = self.nodes[justifiedIdx]
if justified_node.isNone(): if justifiedNode.isNone():
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidJustifiedIndex, kind: fcInvalidJustifiedIndex,
index: justified_index index: justifiedIdx)
)
let best_descendant_index = justified_node.get().best_descendant.get(justified_index) let bestDescendantIdx = justifiedNode.get().bestDescendant.get(justifiedIdx)
let best_node = self.nodes[best_descendant_index] let bestNode = self.nodes[bestDescendantIdx]
if best_node.isNone(): if bestNode.isNone():
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidBestDescendant, kind: fcInvalidBestDescendant,
index: best_descendant_index index: bestDescendantIdx)
)
# Perform a sanity check to ensure the node can be head # Perform a sanity check to ensure the node can be head
if not self.node_is_viable_for_head(best_node.get()): if not self.nodeIsViableForHead(bestNode.get()):
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidBestNode, kind: fcInvalidBestNode,
start_root: justified_root, startRoot: justifiedRoot,
justified_epoch: self.justified_epoch, justifiedEpoch: self.justifiedEpoch,
finalized_epoch: self.finalized_epoch, finalizedEpoch: self.finalizedEpoch,
head_root: justified_node.get().root, headRoot: justifiedNode.get().root,
head_justified_epoch: justified_node.get().justified_epoch, headJustifiedEpoch: justifiedNode.get().justifiedEpoch,
head_finalized_epoch: justified_node.get().finalized_epoch headFinalizedEpoch: justifiedNode.get().finalizedEpoch)
)
head = best_node.get().root head = bestNode.get().root
return ok() ok()
func prune*( func prune*(self: var ProtoArray, finalizedRoot: Eth2Digest): FcResult[void] =
self: var ProtoArray,
finalized_root: Eth2Digest
): FcResult[void] =
## Update the tree with new finalization information. ## Update the tree with new finalization information.
## The tree is pruned if and only if: ## The tree is pruned if and only if:
## - The `finalized_root` and finalized epoch are different from current ## - The `finalizedRoot` and finalized epoch are different from current
## ##
## Returns error if: ## Returns error if:
## - The finalized epoch is less than the current one ## - 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 finalized root is different
## - Internal error due to invalid indices in `self` ## - Internal error due to invalid indices in `self`
var finalized_index: int var finalizedIdx: int
self.indices.withValue(finalized_root, value) do: self.indices.withValue(finalizedRoot, value) do:
finalized_index = value[] finalizedIdx = value[]
do: do:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcFinalizedNodeUnknown, kind: fcFinalizedNodeUnknown,
block_root: finalized_root blockRoot: finalizedRoot)
)
if finalized_index == self.nodes.offset: if finalizedIdx == self.nodes.offset:
# Nothing to do # Nothing to do
return ok() return ok()
if finalized_index < self.nodes.offset: if finalizedIdx < self.nodes.offset:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcPruningFromOutdatedFinalizedRoot, kind: fcPruningFromOutdatedFinalizedRoot,
finalizedRoot: finalized_root finalizedRoot: finalizedRoot)
)
trace "Pruning blocks from fork choice", trace "Pruning blocks from fork choice",
finalizedRoot = shortlog(finalized_root) finalizedRoot = shortlog(finalizedRoot)
let final_phys_index = finalized_index-self.nodes.offset let finalPhysicalIdx = finalizedIdx - self.nodes.offset
for node_index in 0 ..< final_phys_index: for nodeIdx in 0 ..< finalPhysicalIdx:
self.indices.del(self.nodes.buf[node_index].root) self.indices.del(self.nodes.buf[nodeIdx].root)
# Drop all nodes prior to finalization. # Drop all nodes prior to finalization.
# This is done in-place with `moveMem` to avoid costly reallocations. # This is done in-place with `moveMem` to avoid costly reallocations.
static: doAssert ProtoNode.supportsCopyMem(), "ProtoNode must be a trivial type" static: doAssert ProtoNode.supportsCopyMem(), "ProtoNode must be a trivial type"
let tail = self.nodes.len - final_phys_index let tail = self.nodes.len - finalPhysicalIdx
# TODO: can we have an unallocated `self.nodes`? i.e. self.nodes[0] is nil # TODO: can we have an unallocated `self.nodes`? i.e. self.nodes[0] is nil
moveMem(self.nodes.buf[0].addr, self.nodes.buf[final_phys_index].addr, tail * sizeof(ProtoNode)) moveMem(self.nodes.buf[0].addr, self.nodes.buf[finalPhysicalIdx].addr, tail * sizeof(ProtoNode))
self.nodes.buf.setLen(tail) self.nodes.buf.setLen(tail)
# update offset # update offset
self.nodes.offset = finalized_index self.nodes.offset = finalizedIdx
return ok() ok()
func maybeUpdateBestChildAndDescendant(self: var ProtoArray,
func maybe_update_best_child_and_descendant( parentIdx: Index,
self: var ProtoArray, childIdx: Index): FcResult[void] =
parent_index: Index, ## Observe the parent at `parentIdx` with respect to the child at `childIdx` and
child_index: Index): Result[void, ForkChoiceError] = ## potentially modify the `parent.bestChild` and `parent.bestDescendant` values
## Observe the parent at `parent_index` with respect to the child at `child_index` and
## potentially modify the `parent.best_child` and `parent.best_descendant` values
## ##
## There are four scenarios: ## There are four scenarios:
## ##
@ -362,120 +354,113 @@ func maybe_update_best_child_and_descendant(
## 3. The child is not the best child but becomes the best child ## 3. The child is not the best child but becomes the best child
## 4. The child is not the best child and does not become the best child ## 4. The child is not the best child and does not become the best child
let child = self.nodes[child_index] let child = self.nodes[childIdx]
if child.isNone(): if child.isNone():
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidNodeIndex, kind: fcInvalidNodeIndex,
index: child_index index: childIdx)
)
let parent = self.nodes[parent_index] let parent = self.nodes[parentIdx]
if parent.isNone(): if parent.isNone():
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidNodeIndex, kind: fcInvalidNodeIndex,
index: parent_index index: parentIdx)
)
let child_leads_to_viable_head = ? self.node_leads_to_viable_head(child.get()) let childLeadsToViableHead = ? self.nodeLeadsToViableHead(child.get())
let # Aliases to the 3 possible (best_child, best_descendant) tuples let # Aliases to the 3 possible (bestChild, bestDescendant) tuples
change_to_none = (none(Index), none(Index)) changeToNone = (none(Index), none(Index))
change_to_child = ( changeToChild = (
some(child_index), some(childIdx),
# Nim `options` module doesn't implement option `or` # Nim `options` module doesn't implement option `or`
if child.get().best_descendant.isSome(): child.get().best_descendant if child.get().bestDescendant.isSome(): child.get().bestDescendant
else: some(child_index) else: some(childIdx)
) )
no_change = (parent.get().best_child, parent.get().best_descendant) noChange = (parent.get().bestChild, parent.get().bestDescendant)
# TODO: state-machine? The control-flow is messy # TODO: state-machine? The control-flow is messy
let (new_best_child, new_best_descendant) = block: let (newBestChild, newBestDescendant) = block:
if parent.get().best_child.isSome: if parent.get().bestChild.isSome:
let best_child_index = parent.get().best_child.unsafeGet() let bestChildIdx = parent.get().bestChild.unsafeGet()
if best_child_index == child_index and not child_leads_to_viable_head: if bestChildIdx == childIdx and not childLeadsToViableHead:
# The child is already the best-child of the parent # The child is already the best-child of the parent
# but it's not viable to be the head block => remove it # but it's not viable to be the head block => remove it
change_to_none changeToNone
elif best_child_index == child_index: elif bestChildIdx == childIdx:
# If the child is the best-child already, set it again to ensure # If the child is the best-child already, set it again to ensure
# that the best-descendant of the parent is up-to-date. # that the best-descendant of the parent is up-to-date.
change_to_child changeToChild
else: else:
let best_child = self.nodes[best_child_index] let bestChild = self.nodes[bestChildIdx]
if best_child.isNone(): if bestChild.isNone():
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidBestDescendant, kind: fcInvalidBestDescendant,
index: best_child_index index: bestChildIdx)
)
let best_child_leads_to_viable_head = let bestChildLeadsToViableHead =
? self.node_leads_to_viable_head(best_child.get()) ? self.nodeLeadsToViableHead(bestChild.get())
if child_leads_to_viable_head and not best_child_leads_to_viable_head: if childLeadsToViableHead and not bestChildLeadsToViableHead:
# The child leads to a viable head, but the current best-child doesn't # The child leads to a viable head, but the current best-child doesn't
change_to_child changeToChild
elif not child_leads_to_viable_head and best_child_leads_to_viable_head: elif not childLeadsToViableHead and bestChildLeadsToViableHead:
# The best child leads to a viable head, but the child doesn't # The best child leads to a viable head, but the child doesn't
no_change noChange
elif child.get().weight == best_child.get().weight: elif child.get().weight == bestChild.get().weight:
# Tie-breaker of equal weights by root # Tie-breaker of equal weights by root
if child.get().root.tiebreak(best_child.get().root): if child.get().root.tiebreak(bestChild.get().root):
change_to_child changeToChild
else: else:
no_change noChange
else: # Choose winner by weight else: # Choose winner by weight
let cw = child.get().weight let cw = child.get().weight
let bw = best_child.get().weight let bw = bestChild.get().weight
if cw >= bw: if cw >= bw:
change_to_child changeToChild
else: else:
no_change noChange
else: else:
if child_leads_to_viable_head: if childLeadsToViableHead:
# There is no current best-child and the child is viable # There is no current best-child and the child is viable
change_to_child changeToChild
else: else:
# There is no current best-child but the child is not viable # There is no current best-child but the child is not viable
no_change noChange
self.nodes.buf[parent_index - self.nodes.offset].best_child = new_best_child self.nodes.buf[parentIdx - self.nodes.offset].bestChild = newBestChild
self.nodes.buf[parent_index - self.nodes.offset].best_descendant = new_best_descendant self.nodes.buf[parentIdx - self.nodes.offset].bestDescendant = newBestDescendant
return ok() ok()
func node_leads_to_viable_head( func nodeLeadsToViableHead(self: ProtoArray, node: ProtoNode): FcResult[bool] =
self: ProtoArray, node: ProtoNode
): FcResult[bool] =
## Indicates if the node itself or its best-descendant are viable ## Indicates if the node itself or its best-descendant are viable
## for blockchain head ## for blockchain head
let best_descendant_is_viable_for_head = block: let bestDescendantIsViableForHead = block:
if node.best_descendant.isSome(): if node.bestDescendant.isSome():
let best_descendant_index = node.best_descendant.unsafeGet() let bestDescendantIdx = node.bestDescendant.unsafeGet()
let best_descendant = self.nodes[best_descendant_index] let bestDescendant = self.nodes[bestDescendantIdx]
if best_descendant.isNone: if bestDescendant.isNone:
return err ForkChoiceError( return err ForkChoiceError(
kind: fcInvalidBestDescendant, kind: fcInvalidBestDescendant,
index: best_descendant_index index: bestDescendantIdx)
) self.nodeIsViableForHead(bestDescendant.get())
self.node_is_viable_for_head(best_descendant.get())
else: else:
false false
return ok(best_descendant_is_viable_for_head or ok(bestDescendantIsViableForHead or self.nodeIsViableForHead(node))
self.node_is_viable_for_head(node))
func node_is_viable_for_head(self: ProtoArray, node: ProtoNode): bool = func nodeIsViableForHead(self: ProtoArray, node: ProtoNode): bool =
## This is the equivalent of `filter_block_tree` function in eth2 spec ## This is the equivalent of `filter_block_tree` function in eth2 spec
## https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/fork-choice.md#filter_block_tree ## https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/fork-choice.md#filter_block_tree
## ##
## Any node that has a different finalized or justified epoch ## Any node that has a different finalized or justified epoch
## should not be viable for the head. ## should not be viable for the head.
( (
(node.justified_epoch == self.justified_epoch) or (node.justifiedEpoch == self.justifiedEpoch) or
(self.justified_epoch == Epoch(0)) (self.justifiedEpoch == Epoch(0))
) and ( ) and (
(node.finalized_epoch == self.finalized_epoch) or (node.finalizedEpoch == self.finalizedEpoch) or
(self.finalized_epoch == Epoch(0)) (self.finalizedEpoch == Epoch(0))
) )
# Sanity checks # Sanity checks
@ -493,14 +478,12 @@ when isMainModule:
doAssert tiebreak(a, b) doAssert tiebreak(a, b)
block: block:
let a = Eth2Digest.fromHex("0x0000000000000002000000000000000000000000000000000000000000000000") let a = Eth2Digest.fromHex("0x0000000000000002000000000000000000000000000000000000000000000000")
let b = Eth2Digest.fromHex("0x0000000000000001000000000000000000000000000000000000000000000000") # sha256(1) let b = Eth2Digest.fromHex("0x0000000000000001000000000000000000000000000000000000000000000000") # sha256(1)
doAssert tiebreak(a, b) doAssert tiebreak(a, b)
block: block:
let a = Eth2Digest.fromHex("0xD86E8112F3C4C4442126F8E9F44F16867DA487F29052BF91B810457DB34209A4") # sha256(2) let a = Eth2Digest.fromHex("0xD86E8112F3C4C4442126F8E9F44F16867DA487F29052BF91B810457DB34209A4") # sha256(2)
let b = Eth2Digest.fromHex("0x7C9FA136D4413FA6173637E883B6998D32E1D675F88CDDFF9DCBCF331820F4B8") # sha256(1) let b = Eth2Digest.fromHex("0x7C9FA136D4413FA6173637E883B6998D32E1D675F88CDDFF9DCBCF331820F4B8") # sha256(1)