prysm-pulse/beacon-chain/forkchoice/protoarray/nodes.go

154 lines
5.7 KiB
Go

package protoarray
import (
"bytes"
"context"
"go.opencensus.io/trace"
)
// updateBestChildAndDescendant updates parent node's best child and descendent.
// It looks at input parent node and input child node and potentially modifies parent's best
// child and best descendent indices.
// There are four outcomes:
// 1.) The child is already the best child but it's now invalid due to a FFG change and should be removed.
// 2.) The child is already the best child and the parent is updated with the new best descendant.
// 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 best child.
func (s *Store) updateBestChildAndDescendant(ctx context.Context, parentIndex uint64, childIndex uint64) error {
ctx, span := trace.StartSpan(ctx, "protoArrayForkChoice.updateBestChildAndDescendant")
defer span.End()
// Protection against parent index out of bound, this should not happen.
if parentIndex >= uint64(len(s.nodes)) {
return errInvalidNodeIndex
}
parent := s.nodes[parentIndex]
// Protection against child index out of bound, again this should not happen.
if childIndex >= uint64(len(s.nodes)) {
return errInvalidNodeIndex
}
child := s.nodes[childIndex]
// Is the child viable to become head? Based on justification and finalization rules.
childLeadsToViableHead, err := s.leadsToViableHead(ctx, child)
if err != nil {
return err
}
// Define 3 variables for the 3 outcomes mentioned above. This is to
// set `parent.bestChild` and `parent.bestDescendent` to. These
// aliases are to assist readability.
changeToNone := []uint64{nonExistentNode, nonExistentNode}
bestDescendant := child.bestDescendant
if bestDescendant == nonExistentNode {
bestDescendant = childIndex
}
changeToChild := []uint64{childIndex, bestDescendant}
noChange := []uint64{parent.bestChild, parent.bestDescendant}
newParentChild := make([]uint64, 0)
if parent.bestChild != nonExistentNode {
if parent.bestChild == childIndex && !childLeadsToViableHead {
// If the child is already the best child of the parent but it's not viable for head,
// we should remove it. (Outcome 1)
newParentChild = changeToNone
} else if parent.bestChild == childIndex {
// If the child is already the best child of the parent, set it again to ensure best
// descendent of the parent is updated. (Outcome 2)
newParentChild = changeToChild
} else {
// Protection against parent's best child going out of bound.
if parent.bestChild > uint64(len(s.nodes)) {
return errInvalidBestDescendantIndex
}
bestChild := s.nodes[parent.bestChild]
// Is current parent's best child viable to be head? Based on justification and finalization rules.
bestChildLeadsToViableHead, err := s.leadsToViableHead(ctx, bestChild)
if err != nil {
return err
}
if childLeadsToViableHead && !bestChildLeadsToViableHead {
// The child leads to a viable head, but the current parent's best child doesnt.
newParentChild = changeToChild
} else if !childLeadsToViableHead && bestChildLeadsToViableHead {
// The child doesn't lead to a viable head, the current parent's best child does.
newParentChild = noChange
} else if child.weight == bestChild.weight {
// If both are viable, compare their weights.
// Tie-breaker of equal weights by root.
if bytes.Compare(child.root[:], bestChild.root[:]) > 0 {
newParentChild = changeToChild
} else {
newParentChild = noChange
}
} else {
// Choose winner by weight.
if child.weight > bestChild.weight {
newParentChild = changeToChild
} else {
newParentChild = noChange
}
}
}
} else {
if childLeadsToViableHead {
// If parent doesn't have a best child and the child is viable.
newParentChild = changeToChild
} else {
// If parent doesn't have a best child and the child is not viable.
newParentChild = noChange
}
}
// Update parent with the outcome.
parent.bestChild = newParentChild[0]
parent.bestDescendant = newParentChild[1]
s.nodes[parentIndex] = parent
return nil
}
// leadsToViableHead returns true if the node or the best descendent of the node is viable for head.
// Any node with diff finalized or justified epoch than the ones in fork choice store
// should not be viable to head.
func (s *Store) leadsToViableHead(ctx context.Context, node *Node) (bool, error) {
ctx, span := trace.StartSpan(ctx, "protoArrayForkChoice.leadsToViableHead")
defer span.End()
var bestDescendentViable bool
bestDescendentIndex := node.bestDescendant
// If the best descendant is not part of the leaves.
if bestDescendentIndex != nonExistentNode {
// Protection against out of bound, best descendent index can not be
// exceeds length of nodes list.
if bestDescendentIndex >= uint64(len(s.nodes)) {
return false, errInvalidBestDescendantIndex
}
bestDescendentNode := s.nodes[bestDescendentIndex]
bestDescendentViable = s.viableForHead(ctx, bestDescendentNode)
}
// The node is viable as long as the best descendent is viable.
return bestDescendentViable || s.viableForHead(ctx, node), nil
}
// viableForHead returns true if the node is viable to head.
// Any node with diff finalized or justified epoch than the ones in fork choice store
// should not be viable to head.
func (s *Store) viableForHead(ctx context.Context, node *Node) bool {
ctx, span := trace.StartSpan(ctx, "protoArrayForkChoice.viableForHead")
defer span.End()
// `node` is viable if its justified epoch and finalized epoch are the same as the one in `Store`.
// It's also viable if we are in genesis epoch.
justified := s.justifiedEpoch == node.justifiedEpoch || s.justifiedEpoch == 0
finalized := s.finalizedEpoch == node.finalizedEpoch || s.finalizedEpoch == 0
return justified && finalized
}