package consensus import ( "errors" "sync" "time" . "github.com/tendermint/tendermint/binary" . "github.com/tendermint/tendermint/blocks" . "github.com/tendermint/tendermint/common" "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/state" ) const ( RoundStepStart = uint8(0x00) // Round started. RoundStepPropose = uint8(0x01) // Did propose, broadcasting proposal. RoundStepVote = uint8(0x02) // Did vote, broadcasting votes. RoundStepPrecommit = uint8(0x03) // Did precommit, broadcasting precommits. RoundStepCommit = uint8(0x04) // We committed at this round -- do not progress to the next round. ) var ( ErrInvalidProposalSignature = errors.New("Error invalid proposal signature") consensusStateKey = []byte("consensusState") ) // Immutable when returned from ConsensusState.GetRoundState() type RoundState struct { Height uint32 // Height we are working on Round uint16 Step uint8 StartTime time.Time Validators *state.ValidatorSet Proposer *state.Validator Proposal *Proposal ProposalBlock *Block ProposalBlockPartSet *PartSet ProposalPOL *POL ProposalPOLPartSet *PartSet LockedBlock *Block LockedPOL *POL Votes *VoteSet Precommits *VoteSet Commits *VoteSet PrivValidator *PrivValidator } //------------------------------------- // Tracks consensus state across block heights and rounds. type ConsensusState struct { blockStore *BlockStore mempool *mempool.Mempool mtx sync.Mutex RoundState state *state.State // State until height-1. stagedBlock *Block // Cache last staged block. stagedState *state.State // Cache result of staged block. } func NewConsensusState(state *state.State, blockStore *BlockStore, mempool *mempool.Mempool) *ConsensusState { cs := &ConsensusState{ blockStore: blockStore, mempool: mempool, } cs.updateToState(state) return cs } func (cs *ConsensusState) GetRoundState() *RoundState { cs.mtx.Lock() defer cs.mtx.Unlock() rs := cs.RoundState // copy return &rs } func (cs *ConsensusState) updateToState(state *state.State) { // Sanity check state. stateHeight := state.Height if stateHeight > 0 && stateHeight != cs.Height+1 { Panicf("updateToState() expected state height of %v but found %v", cs.Height+1, stateHeight) } // Reset fields based on state. height := state.Height validators := state.BondedValidators cs.Height = height cs.Round = 0 cs.Step = RoundStepStart cs.StartTime = state.CommitTime.Add(newBlockWaitDuration) cs.Validators = validators cs.Proposer = validators.Proposer() cs.Proposal = nil cs.ProposalBlock = nil cs.ProposalBlockPartSet = nil cs.ProposalPOL = nil cs.ProposalPOLPartSet = nil cs.LockedBlock = nil cs.LockedPOL = nil cs.Votes = NewVoteSet(height, 0, VoteTypeBare, validators) cs.Precommits = NewVoteSet(height, 0, VoteTypePrecommit, validators) cs.Commits = NewVoteSet(height, 0, VoteTypeCommit, validators) cs.state = state cs.stagedBlock = nil cs.stagedState = nil // Update the round if we need to. round := calcRound(cs.StartTime) if round > 0 { cs.setupRound(round) } } func (cs *ConsensusState) SetupRound(round uint16) { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.Round >= round { Panicf("ConsensusState round %v not lower than desired round %v", cs.Round, round) } cs.setupRound(round) } func (cs *ConsensusState) setupRound(round uint16) { // Increment all the way to round. validators := cs.Validators.Copy() for r := cs.Round; r < round; r++ { validators.IncrementAccum() } cs.Round = round cs.Step = RoundStepStart cs.Validators = validators cs.Proposer = validators.Proposer() cs.Proposal = nil cs.ProposalBlock = nil cs.ProposalBlockPartSet = nil cs.ProposalPOL = nil cs.ProposalPOLPartSet = nil cs.Votes = NewVoteSet(cs.Height, round, VoteTypeBare, validators) cs.Votes.AddFromCommits(cs.Commits) cs.Precommits = NewVoteSet(cs.Height, round, VoteTypePrecommit, validators) cs.Precommits.AddFromCommits(cs.Commits) } func (cs *ConsensusState) SetStep(step byte) { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.Step < step { cs.Step = step } else { panic("step regression") } } func (cs *ConsensusState) SetPrivValidator(priv *PrivValidator) { cs.mtx.Lock() defer cs.mtx.Unlock() cs.PrivValidator = priv } func (cs *ConsensusState) SetProposal(proposal *Proposal) error { cs.mtx.Lock() defer cs.mtx.Unlock() // Already have one if cs.Proposal != nil { return nil } // Invalid. if proposal.Height != cs.Height || proposal.Round != cs.Round { return nil } // Verify signature if !cs.Proposer.Verify(proposal) { return ErrInvalidProposalSignature } cs.Proposal = proposal cs.ProposalBlockPartSet = NewPartSetFromMetadata(proposal.BlockPartsTotal, proposal.BlockPartsHash) cs.ProposalPOLPartSet = NewPartSetFromMetadata(proposal.POLPartsTotal, proposal.POLPartsHash) return nil } func (cs *ConsensusState) MakeProposal() { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.PrivValidator == nil || cs.Proposer.Id != cs.PrivValidator.Id { return } var block *Block var blockPartSet *PartSet var pol *POL var polPartSet *PartSet // Decide on block and POL if cs.LockedBlock != nil { // If we're locked onto a block, just choose that. block = cs.LockedBlock pol = cs.LockedPOL } else { // TODO: make use of state returned from MakeProposalBlock() block, _ = cs.mempool.MakeProposalBlock() pol = cs.LockedPOL // If exists, is a PoUnlock. } blockPartSet = NewPartSetFromData(BinaryBytes(block)) if pol != nil { polPartSet = NewPartSetFromData(BinaryBytes(pol)) } // Make proposal proposal := NewProposal(cs.Height, cs.Round, blockPartSet.Total(), blockPartSet.RootHash(), polPartSet.Total(), polPartSet.RootHash()) cs.PrivValidator.Sign(proposal) // Set fields cs.Proposal = proposal cs.ProposalBlock = block cs.ProposalBlockPartSet = blockPartSet cs.ProposalPOL = pol cs.ProposalPOLPartSet = polPartSet } // NOTE: block is not necessarily valid. func (cs *ConsensusState) AddProposalBlockPart(height uint32, round uint16, part *Part) (added bool, err error) { cs.mtx.Lock() defer cs.mtx.Unlock() // Blocks might be reused, so round mismatch is OK if cs.Height != height { return false, nil } // We're not expecting a block part. if cs.ProposalBlockPartSet != nil { return false, nil // TODO: bad peer? Return error? } added, err = cs.ProposalBlockPartSet.AddPart(part) if err != nil { return added, err } if added && cs.ProposalBlockPartSet.IsComplete() { var n int64 var err error cs.ProposalBlock = ReadBlock(cs.ProposalBlockPartSet.GetReader(), &n, &err) return true, err } return true, nil } // NOTE: POL is not necessarily valid. func (cs *ConsensusState) AddProposalPOLPart(height uint32, round uint16, part *Part) (added bool, err error) { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.Height != height || cs.Round != round { return false, nil } // We're not expecting a POL part. if cs.ProposalPOLPartSet != nil { return false, nil // TODO: bad peer? Return error? } added, err = cs.ProposalPOLPartSet.AddPart(part) if err != nil { return added, err } if added && cs.ProposalPOLPartSet.IsComplete() { var n int64 var err error cs.ProposalPOL = ReadPOL(cs.ProposalPOLPartSet.GetReader(), &n, &err) return true, err } return true, nil } func (cs *ConsensusState) AddVote(vote *Vote) (added bool, err error) { switch vote.Type { case VoteTypeBare: // Votes checks for height+round match. return cs.Votes.Add(vote) case VoteTypePrecommit: // Precommits checks for height+round match. return cs.Precommits.Add(vote) case VoteTypeCommit: // Commits checks for height match. cs.Votes.Add(vote) cs.Precommits.Add(vote) return cs.Commits.Add(vote) default: panic("Unknown vote type") } } // Lock the ProposalBlock if we have enough votes for it, // or unlock an existing lock if +2/3 of votes were nil. // Returns a blockhash if a block was locked. func (cs *ConsensusState) LockOrUnlock(height uint32, round uint16) []byte { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.Height != height || cs.Round != round { return nil } if hash, _, ok := cs.Votes.TwoThirdsMajority(); ok { // Remember this POL. (hash may be nil) cs.LockedPOL = cs.Votes.MakePOL() if len(hash) == 0 { // +2/3 voted nil. Just unlock. cs.LockedBlock = nil return nil } else if cs.ProposalBlock.HashesTo(hash) { // +2/3 voted for proposal block // Validate the block. // See note on ZombieValidators to see why. if cs.stageBlock(cs.ProposalBlock) != nil { log.Warning("+2/3 voted for an invalid block.") return nil } cs.LockedBlock = cs.ProposalBlock return hash } else if cs.LockedBlock.HashesTo(hash) { // +2/3 voted for already locked block // cs.LockedBlock = cs.LockedBlock return hash } else { // We don't have the block that hashes to hash. // Unlock if we're locked. cs.LockedBlock = nil return nil } } else { return nil } } func (cs *ConsensusState) Commit(height uint32, round uint16) *Block { cs.mtx.Lock() defer cs.mtx.Unlock() if cs.Height != height || cs.Round != round { return nil } if hash, commitTime, ok := cs.Precommits.TwoThirdsMajority(); ok { // There are some strange cases that shouldn't happen // (unless voters are duplicitous). // For example, the hash may not be the one that was // proposed this round. These cases should be identified // and warn the administrator. We should err on the side of // caution and not, for example, sign a block. // TODO: Identify these strange cases. var block *Block if cs.LockedBlock.HashesTo(hash) { block = cs.LockedBlock } else if cs.ProposalBlock.HashesTo(hash) { block = cs.ProposalBlock } else { return nil } // The proposal must be valid. if err := cs.stageBlock(block); err != nil { log.Warning("Network is commiting an invalid proposal? %v", err) return nil } // Save to blockStore err := cs.blockStore.SaveBlock(block) if err != nil { return nil } // What was staged becomes committed. state := cs.stagedState state.Save(commitTime) cs.updateToState(state) // Update mempool. cs.mempool.ResetForBlockAndState(block, state) return block } return nil } func (cs *ConsensusState) stageBlock(block *Block) error { // Already staged? if cs.stagedBlock == block { return nil } // Create a copy of the state for staging stateCopy := cs.state.Copy() // Commit block onto the copied state. // NOTE: Basic validation is done in state.AppendBlock(). err := stateCopy.AppendBlock(block, true) if err != nil { return err } else { cs.stagedBlock = block cs.stagedState = stateCopy return nil } }