package consensus import ( "strings" "sync" "github.com/tendermint/tendermint/types" cmn "github.com/tendermint/tmlibs/common" ) type RoundVoteSet struct { Prevotes *types.VoteSet Precommits *types.VoteSet } /* Keeps track of all VoteSets from round 0 to round 'round'. Also keeps track of up to one RoundVoteSet greater than 'round' from each peer, to facilitate catchup syncing of commits. A commit is +2/3 precommits for a block at a round, but which round is not known in advance, so when a peer provides a precommit for a round greater than mtx.round, we create a new entry in roundVoteSets but also remember the peer to prevent abuse. We let each peer provide us with up to 2 unexpected "catchup" rounds. One for their LastCommit round, and another for the official commit round. */ type HeightVoteSet struct { chainID string height int valSet *types.ValidatorSet mtx sync.Mutex round int // max tracked round roundVoteSets map[int]RoundVoteSet // keys: [0...round] peerCatchupRounds map[string][]int // keys: peer.Key; values: at most 2 rounds } func NewHeightVoteSet(chainID string, height int, valSet *types.ValidatorSet) *HeightVoteSet { hvs := &HeightVoteSet{ chainID: chainID, } hvs.Reset(height, valSet) return hvs } func (hvs *HeightVoteSet) Reset(height int, valSet *types.ValidatorSet) { hvs.mtx.Lock() defer hvs.mtx.Unlock() hvs.height = height hvs.valSet = valSet hvs.roundVoteSets = make(map[int]RoundVoteSet) hvs.peerCatchupRounds = make(map[string][]int) hvs.addRound(0) hvs.round = 0 } func (hvs *HeightVoteSet) Height() int { hvs.mtx.Lock() defer hvs.mtx.Unlock() return hvs.height } func (hvs *HeightVoteSet) Round() int { hvs.mtx.Lock() defer hvs.mtx.Unlock() return hvs.round } // Create more RoundVoteSets up to round. func (hvs *HeightVoteSet) SetRound(round int) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if hvs.round != 0 && (round < hvs.round+1) { cmn.PanicSanity("SetRound() must increment hvs.round") } for r := hvs.round + 1; r <= round; r++ { if _, ok := hvs.roundVoteSets[r]; ok { continue // Already exists because peerCatchupRounds. } hvs.addRound(r) } hvs.round = round } func (hvs *HeightVoteSet) addRound(round int) { if _, ok := hvs.roundVoteSets[round]; ok { cmn.PanicSanity("addRound() for an existing round") } // log.Debug("addRound(round)", "round", round) prevotes := types.NewVoteSet(hvs.chainID, hvs.height, round, types.VoteTypePrevote, hvs.valSet) precommits := types.NewVoteSet(hvs.chainID, hvs.height, round, types.VoteTypePrecommit, hvs.valSet) hvs.roundVoteSets[round] = RoundVoteSet{ Prevotes: prevotes, Precommits: precommits, } } // Duplicate votes return added=false, err=nil. // By convention, peerKey is "" if origin is self. func (hvs *HeightVoteSet) AddVote(vote *types.Vote, peerKey string) (added bool, err error) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(vote.Type) { return } voteSet := hvs.getVoteSet(vote.Round, vote.Type) if voteSet == nil { if rndz := hvs.peerCatchupRounds[peerKey]; len(rndz) < 2 { hvs.addRound(vote.Round) voteSet = hvs.getVoteSet(vote.Round, vote.Type) hvs.peerCatchupRounds[peerKey] = append(rndz, vote.Round) } else { // Peer has sent a vote that does not match our round, // for more than one round. Bad peer! // TODO punish peer. // log.Warn("Deal with peer giving votes from unwanted rounds") return } } added, err = voteSet.AddVote(vote) return } func (hvs *HeightVoteSet) Prevotes(round int) *types.VoteSet { hvs.mtx.Lock() defer hvs.mtx.Unlock() return hvs.getVoteSet(round, types.VoteTypePrevote) } func (hvs *HeightVoteSet) Precommits(round int) *types.VoteSet { hvs.mtx.Lock() defer hvs.mtx.Unlock() return hvs.getVoteSet(round, types.VoteTypePrecommit) } // Last round and blockID that has +2/3 prevotes for a particular block or nil. // Returns -1 if no such round exists. func (hvs *HeightVoteSet) POLInfo() (polRound int, polBlockID types.BlockID) { hvs.mtx.Lock() defer hvs.mtx.Unlock() for r := hvs.round; r >= 0; r-- { rvs := hvs.getVoteSet(r, types.VoteTypePrevote) polBlockID, ok := rvs.TwoThirdsMajority() if ok { return r, polBlockID } } return -1, types.BlockID{} } func (hvs *HeightVoteSet) getVoteSet(round int, type_ byte) *types.VoteSet { rvs, ok := hvs.roundVoteSets[round] if !ok { return nil } switch type_ { case types.VoteTypePrevote: return rvs.Prevotes case types.VoteTypePrecommit: return rvs.Precommits default: cmn.PanicSanity(cmn.Fmt("Unexpected vote type %X", type_)) return nil } } func (hvs *HeightVoteSet) String() string { return hvs.StringIndented("") } func (hvs *HeightVoteSet) StringIndented(indent string) string { hvs.mtx.Lock() defer hvs.mtx.Unlock() vsStrings := make([]string, 0, (len(hvs.roundVoteSets)+1)*2) // rounds 0 ~ hvs.round inclusive for round := 0; round <= hvs.round; round++ { voteSetString := hvs.roundVoteSets[round].Prevotes.StringShort() vsStrings = append(vsStrings, voteSetString) voteSetString = hvs.roundVoteSets[round].Precommits.StringShort() vsStrings = append(vsStrings, voteSetString) } // all other peer catchup rounds for round, roundVoteSet := range hvs.roundVoteSets { if round <= hvs.round { continue } voteSetString := roundVoteSet.Prevotes.StringShort() vsStrings = append(vsStrings, voteSetString) voteSetString = roundVoteSet.Precommits.StringShort() vsStrings = append(vsStrings, voteSetString) } return cmn.Fmt(`HeightVoteSet{H:%v R:0~%v %s %v %s}`, hvs.height, hvs.round, indent, strings.Join(vsStrings, "\n"+indent+" "), indent) } // If a peer claims that it has 2/3 majority for given blockKey, call this. // NOTE: if there are too many peers, or too much peer churn, // this can cause memory issues. // TODO: implement ability to remove peers too func (hvs *HeightVoteSet) SetPeerMaj23(round int, type_ byte, peerID string, blockID types.BlockID) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if !types.IsVoteTypeValid(type_) { return } voteSet := hvs.getVoteSet(round, type_) if voteSet == nil { return } voteSet.SetPeerMaj23(peerID, blockID) }