---
tags: workflow
---
# ForkChoice reaserch
## Рабочая версия - protoarray
Вторая версия активируется параметром
`enable-forkchoice-doubly-linked-tree`
# onBlock
````go
func (s *Service) onBlock(ctx context.Context, signed block.SignedBeaconBlock, blockRoot [32]byte) error {
b := signed.Block()
preState, err := s.getBlockPreState(ctx, b)
...
postState, err := transition.ExecuteStateTransition(ctx, preState, signed)
...
/*
Добавляет полученный блок в ForkChoice
*/
if err := s.insertBlockAndAttestationsToForkChoiceStore(ctx, signed.Block(), blockRoot, postState); err != nil {
return errors.Wrapf(err, "could not insert block %d to fork choice store", signed.Block().Slot())
}
// We add a proposer score boost to fork choice for the block root if applicable, right after
// running a successful state transition for the block.
secondsIntoSlot := uint64(time.Since(s.genesisTime).Seconds()) % params.BeaconConfig().SecondsPerSlot
/*
// BoostProposerRoot sets the block root which should be boosted during
// the LMD fork choice algorithm calculations. This is meant to reward timely,
// proposed blocks which occur before a cutoff interval set to
// SECONDS_PER_SLOT // INTERVALS_PER_SLOT.
*/
if err := s.cfg.ForkChoiceStore.BoostProposerRoot(ctx, &forkchoicetypes.ProposerBoostRootArgs{
BlockRoot: blockRoot,
BlockSlot: signed.Block().Slot(),
CurrentSlot: slots.SinceGenesis(s.genesisTime),
SecondsIntoSlot: secondsIntoSlot,
}); err != nil { return err }
if err := s.savePostStateInfo(ctx, blockRoot, signed, postState, false /* reg sync */); err != nil {return err }
...
...
/*
// Determined the head from the fork choice service and saves its new data
// (head root, head block, and head state) to the local service cache.
*/
headRoot, err := s.updateHead(ctx, balances)
...
headBlock, err := s.cfg.BeaconDB.Block(ctx, headRoot)
...
headState, err := s.cfg.StateGen.StateByRoot(ctx, headRoot)
...
if err := s.saveHead(ctx, headRoot, headBlock, headState); err != nil {
return errors.Wrap(err, "could not save head")
}
...
}
````
## insertBlockAndAttestationsToForkChoiceStore
````go
// This feeds in the block and block's attestations to fork choice store. It's allows fork choice store
// to gain information on the most current chain.
func (s *Service) insertBlockAndAttestationsToForkChoiceStore(ctx context.Context, blk block.BeaconBlock, root [32]byte,
st state.BeaconState) error {
ctx, span := trace.StartSpan(ctx, "blockChain.insertBlockAndAttestationsToForkChoiceStore")
defer span.End()
fCheckpoint := st.FinalizedCheckpoint()
jCheckpoint := st.CurrentJustifiedCheckpoint()
if err := s.insertBlockToForkChoiceStore(ctx, blk, root, fCheckpoint, jCheckpoint); err != nil {
return err
}
// Feed in block's attestations to fork choice store.
for _, a := range blk.Body().Attestations() {
committee, err := helpers.BeaconCommitteeFromState(ctx, st, a.Data.Slot, a.Data.CommitteeIndex)
...
indices, err := attestation.AttestingIndices(a.AggregationBits, committee)
...
s.cfg.ForkChoiceStore.ProcessAttestation(ctx, indices, bytesutil.ToBytes32(a.Data.BeaconBlockRoot), a.Data.Target.Epoch)
}
return nil
}
````
## ProcessAttestation
````go
// ProcessAttestation processes attestation for vote accounting, it iterates around validator indices
// and update their votes accordingly.
func (f *ForkChoice) ProcessAttestation(ctx context.Context, validatorIndices []uint64, blockRoot [32]byte, targetEpoch types.Epoch) {
...
for _, index := range validatorIndices {
// Validator indices will grow the vote cache.
for index >= uint64(len(f.votes)) {
f.votes = append(f.votes, Vote{currentRoot: params.BeaconConfig().ZeroHash, nextRoot: params.BeaconConfig().ZeroHash})
}
// Newly allocated vote if the root fields are untouched.
newVote := f.votes[index].nextRoot == params.BeaconConfig().ZeroHash &&
f.votes[index].currentRoot == params.BeaconConfig().ZeroHash
// Vote gets updated if it's newly allocated or high target epoch.
if newVote || targetEpoch > f.votes[index].nextEpoch {
f.votes[index].nextEpoch = targetEpoch
f.votes[index].nextRoot = blockRoot
}
}
processedAttestationCount.Inc()
}
````
## updateHead
````go
// Determined the head from the fork choice service and saves its new data
// (head root, head block, and head state) to the local service cache.
func (s *Service) updateHead(ctx context.Context, balances []uint64) ([32]byte, error) {
// Get head from the fork choice service.
f := s.store.FinalizedCheckpt()
j := s.store.JustifiedCheckpt()
// To get head before the first justified epoch, the fork choice will start with origin root
// instead of zero hashes.
headStartRoot := bytesutil.ToBytes32(j.Root)
if headStartRoot == params.BeaconConfig().ZeroHash {
headStartRoot = s.originBlockRoot
}
// In order to process head, fork choice store requires justified info.
// If the fork choice store is missing justified block info, a node should
// re-initiate fork choice store using the latest justified info.
// This recovers a fatal condition and should not happen in run time.
if !s.cfg.ForkChoiceStore.HasNode(headStartRoot) {
jb, err := s.cfg.BeaconDB.Block(ctx, headStartRoot)
if err != nil {
return [32]byte{}, err
}
/*
Выбор текущей версии ForkChoice
*/
if features.Get().EnableForkChoiceDoublyLinkedTree {
s.cfg.ForkChoiceStore = doublylinkedtree.New(j.Epoch, f.Epoch)
} else {
s.cfg.ForkChoiceStore = protoarray.New(j.Epoch, f.Epoch, bytesutil.ToBytes32(f.Root))
}
if err := s.insertBlockToForkChoiceStore(ctx, jb.Block(), headStartRoot, f, j); err != nil {
return [32]byte{}, err
}
}
/* !!!!!!!!!!!!
Расчитывает и возвращает роот нового хеда
*/
return s.cfg.ForkChoiceStore.Head(ctx, j.Epoch, headStartRoot, balances, f.Epoch)
}
````
## Head
````go
// Head returns the head root from fork choice store.
// It firsts computes validator's balance changes then recalculates block tree from leaves to root.
func (f *ForkChoice) Head(
ctx context.Context,
justifiedEpoch types.Epoch,
justifiedRoot [32]byte,
justifiedStateBalances []uint64,
finalizedEpoch types.Epoch,
) ([32]byte, error) {
newBalances := justifiedStateBalances
// Using the write lock here because `updateCanonicalNodes` that gets called subsequently requires a write operation.
deltas, newVotes, err := computeDeltas(ctx, f.store.nodesIndices, f.votes, f.balances, newBalances)
...
f.votes = newVotes
/*
Пересчитывает веса и скоры
*/
if err := f.store.applyWeightChanges(ctx, justifiedEpoch, finalizedEpoch, newBalances, deltas); err != nil {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return [32]byte{}, errors.Wrap(err, "Could not apply score changes")
}
f.balances = newBalances
return f.store.head(ctx, justifiedRoot)
^^^^^^^^^^^^^^^^^^^^^^^^^^
}
````
## applyWeightChanges
````go
// applyWeightChanges iterates backwards through the nodes in store. It checks all nodes parent
// and its best child. For each node, it updates the weight with input delta and
// back propagate the nodes' delta to its parents' delta. After scoring changes,
// the best child is then updated along with the best descendant.
func (s *Store) applyWeightChanges(
ctx context.Context, justifiedEpoch, finalizedEpoch types.Epoch, newBalances []uint64, delta []int,
) error {
...
// Update the justified / finalized epochs in store if necessary.
if s.justifiedEpoch != justifiedEpoch || s.finalizedEpoch != finalizedEpoch {
s.justifiedEpoch = justifiedEpoch
s.finalizedEpoch = finalizedEpoch
}
// Proposer score defaults to 0.
proposerScore := uint64(0)
// Iterate backwards through all index to node in store.
var err error
for i := len(s.nodes) - 1; i >= 0; i-- {
n := s.nodes[i]
// There is no need to adjust the balances or manage parent of the zero hash, it
// is an alias to the genesis block.
if n.root == params.BeaconConfig().ZeroHash {
continue
}
nodeDelta := delta[i]
// If we have a node where the proposer boost was previously applied,
// we then decrease the delta by the required score amount.
s.proposerBoostLock.Lock()
if s.previousProposerBoostRoot != params.BeaconConfig().ZeroHash && s.previousProposerBoostRoot == n.root {
nodeDelta -= int(s.previousProposerBoostScore)
}
if s.proposerBoostRoot != params.BeaconConfig().ZeroHash && s.proposerBoostRoot == n.root {
proposerScore, err = computeProposerBoostScore(newBalances)
if err != nil {
s.proposerBoostLock.Unlock()
return err
}
iProposerScore, err := pmath.Int(proposerScore)
if err != nil {
s.proposerBoostLock.Unlock()
return err
}
nodeDelta = nodeDelta + iProposerScore
}
s.proposerBoostLock.Unlock()
// A node's weight can not be negative but the delta can be negative.
if nodeDelta < 0 {
d := uint64(-nodeDelta)
if n.weight < d {
s.proposerBoostLock.RLock()
log.WithFields(logrus.Fields{
"nodeDelta": d,
"nodeRoot": fmt.Sprintf("%#x", bytesutil.Trunc(n.root[:])),
"nodeWeight": n.weight,
"proposerBoostRoot": fmt.Sprintf("%#x", bytesutil.Trunc(s.proposerBoostRoot[:])),
"previousProposerBoostRoot": fmt.Sprintf("%#x", bytesutil.Trunc(s.previousProposerBoostRoot[:])),
"previousProposerBoostScore": s.previousProposerBoostScore,
}).Warning("node with invalid weight, setting it to zero")
s.proposerBoostLock.RUnlock()
n.weight = 0
} else {
n.weight -= d
}
} else {
n.weight += uint64(nodeDelta)
}
// Update parent's best child and descendant if the node has a known parent.
if n.parent != NonExistentNode {
// Protection against node parent index out of bound. This should not happen.
if int(n.parent) >= len(delta) {
return errInvalidParentDelta
}
// Back propagate the nodes' delta to its parent.
delta[n.parent] += nodeDelta
}
}
// Set the previous boosted root and score.
s.proposerBoostLock.Lock()
s.previousProposerBoostRoot = s.proposerBoostRoot
s.previousProposerBoostScore = proposerScore
s.proposerBoostLock.Unlock()
for i := len(s.nodes) - 1; i >= 0; i-- {
n := s.nodes[i]
if n.parent != NonExistentNode {
if int(n.parent) >= len(delta) {
return errInvalidParentDelta
}
if err := s.updateBestChildAndDescendant(n.parent, uint64(i)); err != nil {
return err
}
}
}
return nil
}
````
## updateBestChildAndDescendant
````go
// updateBestChildAndDescendant updates parent node's best child and descendant.
// It looks at input parent node and input child node and potentially modifies parent's best
// child and best descendant 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 the best child.
func (s *Store) updateBestChildAndDescendant(parentIndex, childIndex uint64) error {
// 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(child)
if err != nil {
return err
}
// Define 3 variables for the 3 outcomes mentioned above. This is to
// set `parent.bestChild` and `parent.bestDescendant` 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}
var newParentChild []uint64
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 the best
// descendant 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(bestChild)
if err != nil {
return err
}
if childLeadsToViableHead && !bestChildLeadsToViableHead {
// The child leads to a viable head, but the current parent's best child doesn't.
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
}
````
## head
````go
// head starts from justified root and then follows the best descendant links
// to find the best block for head.
func (s *Store) head(ctx context.Context, justifiedRoot [32]byte) ([32]byte, error) {
ctx, span := trace.StartSpan(ctx, "protoArrayForkChoice.head")
defer span.End()
// Justified index has to be valid in node indices map, and can not be out of bound.
justifiedIndex, ok := s.nodesIndices[justifiedRoot]
if !ok {
return [32]byte{}, errUnknownJustifiedRoot
}
if justifiedIndex >= uint64(len(s.nodes)) {
return [32]byte{}, errInvalidJustifiedIndex
}
justifiedNode := s.nodes[justifiedIndex]
bestDescendantIndex := justifiedNode.bestDescendant
// If the justified node doesn't have a best descendant,
// the best node is itself.
if bestDescendantIndex == NonExistentNode {
bestDescendantIndex = justifiedIndex
}
if bestDescendantIndex >= uint64(len(s.nodes)) {
return [32]byte{}, errInvalidBestDescendantIndex
}
bestNode := s.nodes[bestDescendantIndex]
if !s.viableForHead(bestNode) {
return [32]byte{}, fmt.Errorf("head at slot %d with weight %d is not eligible, finalizedEpoch %d != %d, justifiedEpoch %d != %d",
bestNode.slot, bestNode.weight/10e9, bestNode.finalizedEpoch, s.finalizedEpoch, bestNode.justifiedEpoch, s.justifiedEpoch)
}
// Update metrics.
if bestNode.root != lastHeadRoot {
headChangesCount.Inc()
headSlotNumber.Set(float64(bestNode.slot))
lastHeadRoot = bestNode.root
}
// Update canonical mapping given the head root.
if err := s.updateCanonicalNodes(ctx, bestNode.root); err != nil {
return [32]byte{}, err
}
return bestNode.root, nil
}
````