package consensus import ( "fmt" cstypes "github.com/tendermint/tendermint/consensus/types" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/types" ) // MisbehaviorList encompasses a list of all possible behaviors var MisbehaviorList = map[string]Misbehavior{ "double-prevote": DoublePrevoteMisbehavior(), } type Misbehavior struct { Name string EnterPropose func(cs *State, height int64, round int32) EnterPrevote func(cs *State, height int64, round int32) EnterPrecommit func(cs *State, height int64, round int32) ReceivePrevote func(cs *State, prevote *types.Vote) ReceivePrecommit func(cs *State, precommit *types.Vote) ReceiveProposal func(cs *State, proposal *types.Proposal) error } // BEHAVIORS func DefaultMisbehavior() Misbehavior { return Misbehavior{ Name: "default", EnterPropose: defaultEnterPropose, EnterPrevote: defaultEnterPrevote, EnterPrecommit: defaultEnterPrecommit, ReceivePrevote: defaultReceivePrevote, ReceivePrecommit: defaultReceivePrecommit, ReceiveProposal: defaultReceiveProposal, } } // DoublePrevoteMisbehavior will make a node prevote both nil and a block in the same // height and round. func DoublePrevoteMisbehavior() Misbehavior { b := DefaultMisbehavior() b.Name = "double-prevote" b.EnterPrevote = func(cs *State, height int64, round int32) { // If a block is locked, prevote that. if cs.LockedBlock != nil { cs.Logger.Info("enterPrevote: Already locked on a block, prevoting locked block") cs.signAddVote(tmproto.PrevoteType, cs.LockedBlock.Hash(), cs.LockedBlockParts.Header()) return } // If ProposalBlock is nil, prevote nil. if cs.ProposalBlock == nil { cs.Logger.Info("enterPrevote: ProposalBlock is nil") cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) return } // Validate proposal block err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock) if err != nil { // ProposalBlock is invalid, prevote nil. cs.Logger.Error("enterPrevote: ProposalBlock is invalid", "err", err) cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) return } if cs.sw == nil { cs.Logger.Error("nil switch") return } prevote, err := cs.signVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) if err != nil { cs.Logger.Error("enterPrevote: Unable to sign block", "err", err) } nilPrevote, err := cs.signVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) if err != nil { cs.Logger.Error("enterPrevote: Unable to sign block", "err", err) } // add our own vote cs.sendInternalMessage(msgInfo{&VoteMessage{prevote}, ""}) cs.Logger.Info("Sending conflicting votes") peers := cs.sw.Peers().List() // there has to be at least two other peers connected else this behavior works normally for idx, peer := range peers { if idx%2 == 0 { // sign the proposal block peer.Send(VoteChannel, MustEncode(&VoteMessage{prevote})) } else { // sign a nil block peer.Send(VoteChannel, MustEncode(&VoteMessage{nilPrevote})) } } } return b } // DEFAULTS func defaultEnterPropose(cs *State, height int64, round int32) { logger := cs.Logger.With("height", height, "round", round) // If we don't get the proposal and all block parts quick enough, enterPrevote cs.scheduleTimeout(cs.config.Propose(round), height, round, cstypes.RoundStepPropose) // Nothing more to do if we're not a validator if cs.privValidator == nil { logger.Debug("This node is not a validator") return } logger.Debug("This node is a validator") pubKey, err := cs.privValidator.GetPubKey() if err != nil { // If this node is a validator & proposer in the currentx round, it will // miss the opportunity to create a block. logger.Error("Error on retrival of pubkey", "err", err) return } address := pubKey.Address() // if not a validator, we're done if !cs.Validators.HasAddress(address) { logger.Debug("This node is not a validator", "addr", address, "vals", cs.Validators) return } if cs.isProposer(address) { logger.Info("enterPropose: Our turn to propose", "proposer", address, "privValidator", cs.privValidator) cs.decideProposal(height, round) } else { logger.Info("enterPropose: Not our turn to propose", "proposer", cs.Validators.GetProposer().Address, "privValidator", cs.privValidator) } } func defaultEnterPrevote(cs *State, height int64, round int32) { logger := cs.Logger.With("height", height, "round", round) // If a block is locked, prevote that. if cs.LockedBlock != nil { logger.Info("enterPrevote: Already locked on a block, prevoting locked block") cs.signAddVote(tmproto.PrevoteType, cs.LockedBlock.Hash(), cs.LockedBlockParts.Header()) return } // If ProposalBlock is nil, prevote nil. if cs.ProposalBlock == nil { logger.Info("enterPrevote: ProposalBlock is nil") cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) return } // Validate proposal block err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock) if err != nil { // ProposalBlock is invalid, prevote nil. logger.Error("enterPrevote: ProposalBlock is invalid", "err", err) cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{}) return } // Prevote cs.ProposalBlock // NOTE: the proposal signature is validated when it is received, // and the proposal block parts are validated as they are received (against the merkle hash in the proposal) logger.Info("enterPrevote: ProposalBlock is valid") cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header()) } func defaultEnterPrecommit(cs *State, height int64, round int32) { logger := cs.Logger.With("height", height, "round", round) // check for a polka blockID, ok := cs.Votes.Prevotes(round).TwoThirdsMajority() // If we don't have a polka, we must precommit nil. if !ok { if cs.LockedBlock != nil { logger.Info("enterPrecommit: No +2/3 prevotes during enterPrecommit while we're locked. Precommitting nil") } else { logger.Info("enterPrecommit: No +2/3 prevotes during enterPrecommit. Precommitting nil.") } cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{}) return } // At this point +2/3 prevoted for a particular block or nil. _ = cs.eventBus.PublishEventPolka(cs.RoundStateEvent()) // the latest POLRound should be this round. polRound, _ := cs.Votes.POLInfo() if polRound < round { panic(fmt.Sprintf("This POLRound should be %v but got %v", round, polRound)) } // +2/3 prevoted nil. Unlock and precommit nil. if len(blockID.Hash) == 0 { if cs.LockedBlock == nil { logger.Info("enterPrecommit: +2/3 prevoted for nil.") } else { logger.Info("enterPrecommit: +2/3 prevoted for nil. Unlocking") cs.LockedRound = -1 cs.LockedBlock = nil cs.LockedBlockParts = nil _ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()) } cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{}) return } // At this point, +2/3 prevoted for a particular block. // If we're already locked on that block, precommit it, and update the LockedRound if cs.LockedBlock.HashesTo(blockID.Hash) { logger.Info("enterPrecommit: +2/3 prevoted locked block. Relocking") cs.LockedRound = round _ = cs.eventBus.PublishEventRelock(cs.RoundStateEvent()) cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader) return } // If +2/3 prevoted for proposal block, stage and precommit it if cs.ProposalBlock.HashesTo(blockID.Hash) { logger.Info("enterPrecommit: +2/3 prevoted proposal block. Locking", "hash", blockID.Hash) // Validate the block. if err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock); err != nil { panic(fmt.Sprintf("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) } cs.LockedRound = round cs.LockedBlock = cs.ProposalBlock cs.LockedBlockParts = cs.ProposalBlockParts _ = cs.eventBus.PublishEventLock(cs.RoundStateEvent()) cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader) return } // There was a polka in this round for a block we don't have. // Fetch that block, unlock, and precommit nil. // The +2/3 prevotes for this round is the POL for our unlock. logger.Info("enterPrecommit: +2/3 prevotes for a block we don't have. Voting nil", "blockID", blockID) cs.LockedRound = -1 cs.LockedBlock = nil cs.LockedBlockParts = nil if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) { cs.ProposalBlock = nil cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader) } _ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()) cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{}) } func defaultReceivePrevote(cs *State, vote *types.Vote) { height := cs.Height prevotes := cs.Votes.Prevotes(vote.Round) // If +2/3 prevotes for a block or nil for *any* round: if blockID, ok := prevotes.TwoThirdsMajority(); ok { // There was a polka! // If we're locked but this is a recent polka, unlock. // If it matches our ProposalBlock, update the ValidBlock // Unlock if `cs.LockedRound < vote.Round <= cs.Round` // NOTE: If vote.Round > cs.Round, we'll deal with it when we get to vote.Round if (cs.LockedBlock != nil) && (cs.LockedRound < vote.Round) && (vote.Round <= cs.Round) && !cs.LockedBlock.HashesTo(blockID.Hash) { cs.Logger.Info("Unlocking because of POL.", "lockedRound", cs.LockedRound, "POLRound", vote.Round) cs.LockedRound = -1 cs.LockedBlock = nil cs.LockedBlockParts = nil _ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent()) } // Update Valid* if we can. // NOTE: our proposal block may be nil or not what received a polka.. if len(blockID.Hash) != 0 && (cs.ValidRound < vote.Round) && (vote.Round == cs.Round) { if cs.ProposalBlock.HashesTo(blockID.Hash) { cs.Logger.Info( "Updating ValidBlock because of POL.", "validRound", cs.ValidRound, "POLRound", vote.Round) cs.ValidRound = vote.Round cs.ValidBlock = cs.ProposalBlock cs.ValidBlockParts = cs.ProposalBlockParts } else { cs.Logger.Info( "Valid block we don't know about. Set ProposalBlock=nil", "proposal", cs.ProposalBlock.Hash(), "blockID", blockID.Hash) // We're getting the wrong block. cs.ProposalBlock = nil } if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) { cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader) } cs.evsw.FireEvent(types.EventValidBlock, &cs.RoundState) _ = cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()) } } // If +2/3 prevotes for *anything* for future round: switch { case cs.Round < vote.Round && prevotes.HasTwoThirdsAny(): // Round-skip if there is any 2/3+ of votes ahead of us cs.enterNewRound(height, vote.Round) case cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step: // current round blockID, ok := prevotes.TwoThirdsMajority() if ok && (cs.isProposalComplete() || len(blockID.Hash) == 0) { cs.enterPrecommit(height, vote.Round) } else if prevotes.HasTwoThirdsAny() { cs.enterPrevoteWait(height, vote.Round) } case cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round: // If the proposal is now complete, enter prevote of cs.Round. if cs.isProposalComplete() { cs.enterPrevote(height, cs.Round) } } } func defaultReceivePrecommit(cs *State, vote *types.Vote) { height := cs.Height precommits := cs.Votes.Precommits(vote.Round) cs.Logger.Info("Added to precommit", "vote", vote, "precommits", precommits.StringShort()) blockID, ok := precommits.TwoThirdsMajority() if ok { // Executed as TwoThirdsMajority could be from a higher round cs.enterNewRound(height, vote.Round) cs.enterPrecommit(height, vote.Round) if len(blockID.Hash) != 0 { cs.enterCommit(height, vote.Round) if cs.config.SkipTimeoutCommit && precommits.HasAll() { cs.enterNewRound(cs.Height, 0) } } else { cs.enterPrecommitWait(height, vote.Round) } } else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() { cs.enterNewRound(height, vote.Round) cs.enterPrecommitWait(height, vote.Round) } } func defaultReceiveProposal(cs *State, proposal *types.Proposal) error { // Already have one // TODO: possibly catch double proposals if cs.Proposal != nil { return nil } // Does not apply if proposal.Height != cs.Height || proposal.Round != cs.Round { return nil } // Verify POLRound, which must be -1 or in range [0, proposal.Round). if proposal.POLRound < -1 || (proposal.POLRound >= 0 && proposal.POLRound >= proposal.Round) { return ErrInvalidProposalPOLRound } p := proposal.ToProto() // Verify signature if !cs.Validators.GetProposer().PubKey.VerifySignature( types.ProposalSignBytes(cs.state.ChainID, p), proposal.Signature) { return ErrInvalidProposalSignature } proposal.Signature = p.Signature cs.Proposal = proposal // We don't update cs.ProposalBlockParts if it is already set. // This happens if we're already in cstypes.RoundStepCommit or if there is a valid block in the current round. // TODO: We can check if Proposal is for a different block as this is a sign of misbehavior! if cs.ProposalBlockParts == nil { cs.ProposalBlockParts = types.NewPartSetFromHeader(proposal.BlockID.PartSetHeader) } cs.Logger.Info("Received proposal", "proposal", proposal) return nil }