--- 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 } ````