// (c) 2019-2020, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. package snowman import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" "github.com/ava-labs/gecko/snow/consensus/snowball" ) const ( minMapSize = 16 ) // TopologicalFactory implements Factory by returning a topological struct type TopologicalFactory struct{} // New implements Factory func (TopologicalFactory) New() Consensus { return &Topological{} } // Topological implements the Snowman interface by using a tree tracking the // strongly preferred branch. This tree structure amortizes network polls to // vote on more than just the next block. type Topological struct { metrics // ctx is the context this snowman instance is executing in ctx *snow.Context // params are the parameters that should be used to initialize snowball // instances params snowball.Parameters // head is the last accepted block head ids.ID // blocks stores the last accepted block and all the pending blocks blocks map[[32]byte]*snowmanBlock // blockID -> snowmanBlock // tail is the preferred block with no children tail ids.ID } // Used to track the kahn topological sort status type kahnNode struct { // inDegree is the number of children that haven't been processed yet. If // inDegree is 0, then this node is a leaf inDegree int // votes for all the children of this node, so far votes ids.Bag } // Used to track which children should receive votes type votes struct { // parentID is the parent of all the votes provided in the votes bag parentID ids.ID // votes for all the children of the parent votes ids.Bag } // Initialize implements the Snowman interface func (ts *Topological) Initialize(ctx *snow.Context, params snowball.Parameters, rootID ids.ID) { ts.ctx = ctx ts.params = params if err := ts.metrics.Initialize(ctx.Log, params.Namespace, params.Metrics); err != nil { ts.ctx.Log.Error("%s", err) } ts.head = rootID ts.blocks = map[[32]byte]*snowmanBlock{ rootID.Key(): &snowmanBlock{ sm: ts, }, } ts.tail = rootID } // Parameters implements the Snowman interface func (ts *Topological) Parameters() snowball.Parameters { return ts.params } // Add implements the Snowman interface func (ts *Topological) Add(blk Block) error { parent := blk.Parent() parentID := parent.ID() parentKey := parentID.Key() blkID := blk.ID() blkBytes := blk.Bytes() // Notify anyone listening that this block was issued. ts.ctx.DecisionDispatcher.Issue(ts.ctx.ChainID, blkID, blkBytes) ts.ctx.ConsensusDispatcher.Issue(ts.ctx.ChainID, blkID, blkBytes) ts.metrics.Issued(blkID) parentNode, ok := ts.blocks[parentKey] if !ok { // If the ancestor is missing, this means the ancestor must have already // been pruned. Therefore, the dependent should be transitively // rejected. if err := blk.Reject(); err != nil { return err } // Notify anyone listening that this block was rejected. ts.ctx.DecisionDispatcher.Reject(ts.ctx.ChainID, blkID, blkBytes) ts.ctx.ConsensusDispatcher.Reject(ts.ctx.ChainID, blkID, blkBytes) ts.metrics.Rejected(blkID) return nil } // add the block as a child of its parent, and add the block to the tree parentNode.AddChild(blk) ts.blocks[blkID.Key()] = &snowmanBlock{ sm: ts, blk: blk, } // If we are extending the tail, this is the new tail if ts.tail.Equals(parentID) { ts.tail = blkID } return nil } // Issued implements the Snowman interface func (ts *Topological) Issued(blk Block) bool { // If the block is decided, then it must have been previously issued. if blk.Status().Decided() { return true } // If the block is in the map of current blocks, then the block was issued. _, ok := ts.blocks[blk.ID().Key()] return ok } // Preference implements the Snowman interface func (ts *Topological) Preference() ids.ID { return ts.tail } // RecordPoll implements the Snowman interface // // The votes bag contains at most K votes for blocks in the tree. If there is a // vote for a block that isn't in the tree, the vote is dropped. // // Votes are propagated transitively towards the genesis. All blocks in the tree // that result in at least Alpha votes will record the poll on their children. // Every other block will have an unsuccessful poll registered. // // After collecting which blocks should be voted on, the polls are registered // and blocks are accepted/rejected as needed. The tail is then updated to equal // the leaf on the preferred branch. // // To optimize the theoretical complexity of the vote propagation, a topological // sort is done over the blocks that are reachable from the provided votes. // During the sort, votes are pushed towards the genesis. To prevent interating // over all blocks that had unsuccessful polls, we set a flag on the block to // know that any future traversal through that block should register an // unsuccessful poll on that block and every descendant block. // // The complexity of this function is: // - Runtime = 3 * |live set| + |votes| // - Space = 2 * |live set| + |votes| func (ts *Topological) RecordPoll(votes ids.Bag) error { // Runtime = |live set| + |votes| ; Space = |live set| + |votes| kahnGraph, leaves := ts.calculateInDegree(votes) // Runtime = |live set| ; Space = |live set| voteStack := ts.pushVotes(kahnGraph, leaves) // Runtime = |live set| ; Space = Constant preferred, err := ts.vote(voteStack) if err != nil { return err } // Runtime = |live set| ; Space = Constant ts.tail = ts.getPreferredDecendent(preferred) return nil } // Finalized implements the Snowman interface func (ts *Topological) Finalized() bool { return len(ts.blocks) == 1 } // takes in a list of votes and sets up the topological ordering. Returns the // reachable section of the graph annotated with the number of inbound edges and // the non-transitively applied votes. Also returns the list of leaf blocks. func (ts *Topological) calculateInDegree( votes ids.Bag) (map[[32]byte]kahnNode, []ids.ID) { kahns := make(map[[32]byte]kahnNode, minMapSize) leaves := ids.Set{} for _, vote := range votes.List() { voteKey := vote.Key() votedBlock, validVote := ts.blocks[voteKey] // If the vote is for a block that isn't in the current pending set, // then the vote is dropped if !validVote { continue } // If the vote is for the last accepted block, the vote is dropped if votedBlock.Accepted() { continue } // The parent contains the snowball instance of its children parent := votedBlock.blk.Parent() parentID := parent.ID() parentIDKey := parentID.Key() // Add the votes for this block to the parent's set of responces numVotes := votes.Count(vote) kahn, previouslySeen := kahns[parentIDKey] kahn.votes.AddCount(vote, numVotes) kahns[parentIDKey] = kahn // If the parent block already had registered votes, then there is no // need to iterate into the parents if previouslySeen { continue } // If I've never seen this parent block before, it is currently a leaf. leaves.Add(parentID) // iterate through all the block's ancestors and set up the inDegrees of // the blocks for n := ts.blocks[parentIDKey]; !n.Accepted(); n = ts.blocks[parentIDKey] { parent := n.blk.Parent() parentID := parent.ID() parentIDKey = parentID.Key() // move the loop variable forward // Increase the inDegree by one kahn := kahns[parentIDKey] kahn.inDegree++ kahns[parentIDKey] = kahn // If we have already seen this block, then we shouldn't increase // the inDegree of the ancestors through this block again. if kahn.inDegree != 1 { break } // If I am transitively seeing this block for the first time, either // the block was previously unknown or it was previously a leaf. // Regardless, it shouldn't be tracked as a leaf. leaves.Remove(parentID) } } return kahns, leaves.List() } // convert the tree into a branch of snowball instances with at least alpha // votes func (ts *Topological) pushVotes( kahnNodes map[[32]byte]kahnNode, leaves []ids.ID) []votes { voteStack := []votes(nil) for len(leaves) > 0 { // pop a leaf off the stack newLeavesSize := len(leaves) - 1 leafID := leaves[newLeavesSize] leaves = leaves[:newLeavesSize] // get the block and sort infomation about the block leafIDKey := leafID.Key() kahnNode := kahnNodes[leafIDKey] block := ts.blocks[leafIDKey] // If there are at least Alpha votes, then this block needs to record // the poll on the snowball instance if kahnNode.votes.Len() >= ts.params.Alpha { voteStack = append(voteStack, votes{ parentID: leafID, votes: kahnNode.votes, }) } // If the block is accepted, then we don't need to push votes to the // parent block if block.Accepted() { continue } parent := block.blk.Parent() parentID := parent.ID() parentIDKey := parentID.Key() // Remove an inbound edge from the parent kahn node and push the votes. parentKahnNode := kahnNodes[parentIDKey] parentKahnNode.inDegree-- parentKahnNode.votes.AddCount(leafID, kahnNode.votes.Len()) kahnNodes[parentIDKey] = parentKahnNode // If the inDegree is zero, then the parent node is now a leaf if parentKahnNode.inDegree == 0 { leaves = append(leaves, parentID) } } return voteStack } // apply votes to the branch that received an Alpha threshold func (ts *Topological) vote(voteStack []votes) (ids.ID, error) { // If the voteStack is empty, then the full tree should falter. This won't // change the preferred branch. if len(voteStack) == 0 { ts.ctx.Log.Verbo("No progress was made after a vote with %d pending blocks", len(ts.blocks)-1) headKey := ts.head.Key() headBlock := ts.blocks[headKey] headBlock.shouldFalter = true return ts.tail, nil } // keep track of the new preferred block newPreferred := ts.head onPreferredBranch := true for len(voteStack) > 0 { // pop a vote off the stack newStackSize := len(voteStack) - 1 vote := voteStack[newStackSize] voteStack = voteStack[:newStackSize] // get the block that we are going to vote on voteParentIDKey := vote.parentID.Key() parentBlock, notRejected := ts.blocks[voteParentIDKey] // if the block block we are going to vote on was already rejected, then // we should stop applying the votes if !notRejected { break } // keep track of transitive falters to propagate to this block's // children shouldTransitivelyFalter := parentBlock.shouldFalter // if the block was previously marked as needing to falter, the block // should falter before applying the vote if shouldTransitivelyFalter { ts.ctx.Log.Verbo("Resetting confidence below %s", vote.parentID) parentBlock.sb.RecordUnsuccessfulPoll() parentBlock.shouldFalter = false } // apply the votes for this snowball instance parentBlock.sb.RecordPoll(vote.votes) // Only accept when you are finalized and the head. if parentBlock.sb.Finalized() && ts.head.Equals(vote.parentID) { if err := ts.accept(parentBlock); err != nil { return ids.ID{}, err } // by accepting the child of parentBlock, the last accepted block is // no longer voteParentID, but its child. So, voteParentID can be // removed from the tree. delete(ts.blocks, voteParentIDKey) } // If we are on the preferred branch, then the parent's preference is // the next block on the preferred branch. parentPreference := parentBlock.sb.Preference() if onPreferredBranch { newPreferred = parentPreference } // Get the ID of the child that is having a RecordPoll called. All other // children will need to have their confidence reset. If there isn't a // child having RecordPoll called, then the nextID will default to the // nil ID. nextID := ids.ID{} if len(voteStack) > 0 { nextID = voteStack[newStackSize-1].parentID } // If we are on the preferred branch and the nextID is the preference of // the snowball instance, then we are following the preferred branch. onPreferredBranch = onPreferredBranch && nextID.Equals(parentPreference) // If there wasn't an alpha threshold on the branch (either on this vote // or a past transitive vote), I should falter now. for childIDKey := range parentBlock.children { childID := ids.NewID(childIDKey) // If we don't need to transitively falter and the child is going to // have RecordPoll called on it, then there is no reason to reset // the block's confidence if !shouldTransitivelyFalter && childID.Equals(nextID) { continue } // If we finalized a child of the current block, then all other // children will have been rejected and removed from the tree. // Therefore, we need to make sure the child is still in the tree. childBlock, notRejected := ts.blocks[childIDKey] if notRejected { ts.ctx.Log.Verbo("Defering confidence reset of %s. Voting for %s", childID, nextID) // If the child is ever voted for positively, the confidence // must be reset first. childBlock.shouldFalter = true } } } return newPreferred, nil } // Get the preferred decendent of the provided block ID func (ts *Topological) getPreferredDecendent(blkID ids.ID) ids.ID { // Traverse from the provided ID to the preferred child until there are no // children. for block := ts.blocks[blkID.Key()]; block.sb != nil; block = ts.blocks[blkID.Key()] { blkID = block.sb.Preference() } return blkID } // accept the preferred child of the provided snowman block. By accepting the // preferred child, all other children will be rejected. When these children are // rejected, all their descendants will be rejected. func (ts *Topological) accept(n *snowmanBlock) error { // We are finalizing the block's child, so we need to get the preference pref := n.sb.Preference() ts.ctx.Log.Verbo("Accepting block with ID %s", pref) // Get the child and accept it child := n.children[pref.Key()] if err := child.Accept(); err != nil { return err } // Notify anyone listening that this block was accepted. bytes := child.Bytes() ts.ctx.DecisionDispatcher.Accept(ts.ctx.ChainID, pref, bytes) ts.ctx.ConsensusDispatcher.Accept(ts.ctx.ChainID, pref, bytes) ts.metrics.Accepted(pref) // Because this is the newest accepted block, this is the new head. ts.head = pref // Because ts.blocks contains the last accepted block, we don't delete the // block from the blocks map here. rejects := []ids.ID(nil) for childIDKey, child := range n.children { childID := ids.NewID(childIDKey) if childID.Equals(pref) { // don't reject the block we just accepted continue } if err := child.Reject(); err != nil { return err } // Notify anyone listening that this block was rejected. bytes := child.Bytes() ts.ctx.DecisionDispatcher.Reject(ts.ctx.ChainID, childID, bytes) ts.ctx.ConsensusDispatcher.Reject(ts.ctx.ChainID, childID, bytes) ts.metrics.Rejected(childID) // Track which blocks have been directly rejected rejects = append(rejects, childID) } // reject all the descendants of the blocks we just rejected return ts.rejectTransitively(rejects) } // Takes in a list of rejected ids and rejects all descendants of these IDs func (ts *Topological) rejectTransitively(rejected []ids.ID) error { // the rejected array is treated as a queue, with the next element at index // 0 and the last element at the end of the slice. for len(rejected) > 0 { // pop the rejected ID off the queue newRejectedSize := len(rejected) - 1 rejectedID := rejected[newRejectedSize] rejected = rejected[:newRejectedSize] // get the rejected node, and remove it from the tree rejectedKey := rejectedID.Key() rejectedNode := ts.blocks[rejectedKey] delete(ts.blocks, rejectedKey) for childIDKey, child := range rejectedNode.children { if err := child.Reject(); err != nil { return err } // Notify anyone listening that this block was rejected. childID := ids.NewID(childIDKey) bytes := child.Bytes() ts.ctx.DecisionDispatcher.Reject(ts.ctx.ChainID, childID, bytes) ts.ctx.ConsensusDispatcher.Reject(ts.ctx.ChainID, childID, bytes) ts.metrics.Rejected(childID) // add the newly rejected block to the end of the queue rejected = append(rejected, childID) } } return nil }