package consensus import ( "errors" "fmt" cstypes "github.com/tendermint/tendermint/internal/consensus/types" "github.com/tendermint/tendermint/internal/p2p" "github.com/tendermint/tendermint/libs/bits" tmjson "github.com/tendermint/tendermint/libs/json" tmmath "github.com/tendermint/tendermint/libs/math" tmcons "github.com/tendermint/tendermint/proto/tendermint/consensus" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/types" ) // Message defines an interface that the consensus domain types implement. When // a proto message is received on a consensus p2p Channel, it is wrapped and then // converted to a Message via MsgFromProto. type Message interface { ValidateBasic() error } func init() { tmjson.RegisterType(&NewRoundStepMessage{}, "tendermint/NewRoundStepMessage") tmjson.RegisterType(&NewValidBlockMessage{}, "tendermint/NewValidBlockMessage") tmjson.RegisterType(&ProposalMessage{}, "tendermint/Proposal") tmjson.RegisterType(&ProposalPOLMessage{}, "tendermint/ProposalPOL") tmjson.RegisterType(&BlockPartMessage{}, "tendermint/BlockPart") tmjson.RegisterType(&VoteMessage{}, "tendermint/Vote") tmjson.RegisterType(&HasVoteMessage{}, "tendermint/HasVote") tmjson.RegisterType(&VoteSetMaj23Message{}, "tendermint/VoteSetMaj23") tmjson.RegisterType(&VoteSetBitsMessage{}, "tendermint/VoteSetBits") } // NewRoundStepMessage is sent for every step taken in the ConsensusState. // For every height/round/step transition type NewRoundStepMessage struct { Height int64 Round int32 Step cstypes.RoundStepType SecondsSinceStartTime int64 LastCommitRound int32 } // ValidateBasic performs basic validation. func (m *NewRoundStepMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.Round < 0 { return errors.New("negative Round") } if !m.Step.IsValid() { return errors.New("invalid Step") } // NOTE: SecondsSinceStartTime may be negative // LastCommitRound will be -1 for the initial height, but we don't know what height this is // since it can be specified in genesis. The reactor will have to validate this via // ValidateHeight(). if m.LastCommitRound < -1 { return errors.New("invalid LastCommitRound (cannot be < -1)") } return nil } // ValidateHeight validates the height given the chain's initial height. func (m *NewRoundStepMessage) ValidateHeight(initialHeight int64) error { if m.Height < initialHeight { return fmt.Errorf("invalid Height %v (lower than initial height %v)", m.Height, initialHeight) } if m.Height == initialHeight && m.LastCommitRound != -1 { return fmt.Errorf("invalid LastCommitRound %v (must be -1 for initial height %v)", m.LastCommitRound, initialHeight) } if m.Height > initialHeight && m.LastCommitRound < 0 { return fmt.Errorf("LastCommitRound can only be negative for initial height %v", // nolint initialHeight) } return nil } // String returns a string representation. func (m *NewRoundStepMessage) String() string { return fmt.Sprintf("[NewRoundStep H:%v R:%v S:%v LCR:%v]", m.Height, m.Round, m.Step, m.LastCommitRound) } // NewValidBlockMessage is sent when a validator observes a valid block B in some round r, // i.e., there is a Proposal for block B and 2/3+ prevotes for the block B in the round r. // In case the block is also committed, then IsCommit flag is set to true. type NewValidBlockMessage struct { Height int64 Round int32 BlockPartSetHeader types.PartSetHeader BlockParts *bits.BitArray IsCommit bool } // ValidateBasic performs basic validation. func (m *NewValidBlockMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.Round < 0 { return errors.New("negative Round") } if err := m.BlockPartSetHeader.ValidateBasic(); err != nil { return fmt.Errorf("wrong BlockPartSetHeader: %v", err) } if m.BlockParts.Size() == 0 { return errors.New("empty blockParts") } if m.BlockParts.Size() != int(m.BlockPartSetHeader.Total) { return fmt.Errorf("blockParts bit array size %d not equal to BlockPartSetHeader.Total %d", m.BlockParts.Size(), m.BlockPartSetHeader.Total) } if m.BlockParts.Size() > int(types.MaxBlockPartsCount) { return fmt.Errorf("blockParts bit array is too big: %d, max: %d", m.BlockParts.Size(), types.MaxBlockPartsCount) } return nil } // String returns a string representation. func (m *NewValidBlockMessage) String() string { return fmt.Sprintf("[ValidBlockMessage H:%v R:%v BP:%v BA:%v IsCommit:%v]", m.Height, m.Round, m.BlockPartSetHeader, m.BlockParts, m.IsCommit) } // ProposalMessage is sent when a new block is proposed. type ProposalMessage struct { Proposal *types.Proposal } // ValidateBasic performs basic validation. func (m *ProposalMessage) ValidateBasic() error { return m.Proposal.ValidateBasic() } // String returns a string representation. func (m *ProposalMessage) String() string { return fmt.Sprintf("[Proposal %v]", m.Proposal) } // ProposalPOLMessage is sent when a previous proposal is re-proposed. type ProposalPOLMessage struct { Height int64 ProposalPOLRound int32 ProposalPOL *bits.BitArray } // ValidateBasic performs basic validation. func (m *ProposalPOLMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.ProposalPOLRound < 0 { return errors.New("negative ProposalPOLRound") } if m.ProposalPOL.Size() == 0 { return errors.New("empty ProposalPOL bit array") } if m.ProposalPOL.Size() > types.MaxVotesCount { return fmt.Errorf("proposalPOL bit array is too big: %d, max: %d", m.ProposalPOL.Size(), types.MaxVotesCount) } return nil } // String returns a string representation. func (m *ProposalPOLMessage) String() string { return fmt.Sprintf("[ProposalPOL H:%v POLR:%v POL:%v]", m.Height, m.ProposalPOLRound, m.ProposalPOL) } // BlockPartMessage is sent when gossipping a piece of the proposed block. type BlockPartMessage struct { Height int64 Round int32 Part *types.Part } // ValidateBasic performs basic validation. func (m *BlockPartMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.Round < 0 { return errors.New("negative Round") } if err := m.Part.ValidateBasic(); err != nil { return fmt.Errorf("wrong Part: %v", err) } return nil } // String returns a string representation. func (m *BlockPartMessage) String() string { return fmt.Sprintf("[BlockPart H:%v R:%v P:%v]", m.Height, m.Round, m.Part) } // VoteMessage is sent when voting for a proposal (or lack thereof). type VoteMessage struct { Vote *types.Vote } // ValidateBasic performs basic validation. func (m *VoteMessage) ValidateBasic() error { return m.Vote.ValidateBasic() } // String returns a string representation. func (m *VoteMessage) String() string { return fmt.Sprintf("[Vote %v]", m.Vote) } // HasVoteMessage is sent to indicate that a particular vote has been received. type HasVoteMessage struct { Height int64 Round int32 Type tmproto.SignedMsgType Index int32 } // ValidateBasic performs basic validation. func (m *HasVoteMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.Round < 0 { return errors.New("negative Round") } if !types.IsVoteTypeValid(m.Type) { return errors.New("invalid Type") } if m.Index < 0 { return errors.New("negative Index") } return nil } // String returns a string representation. func (m *HasVoteMessage) String() string { return fmt.Sprintf("[HasVote VI:%v V:{%v/%02d/%v}]", m.Index, m.Height, m.Round, m.Type) } // VoteSetMaj23Message is sent to indicate that a given BlockID has seen +2/3 votes. type VoteSetMaj23Message struct { Height int64 Round int32 Type tmproto.SignedMsgType BlockID types.BlockID } // ValidateBasic performs basic validation. func (m *VoteSetMaj23Message) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if m.Round < 0 { return errors.New("negative Round") } if !types.IsVoteTypeValid(m.Type) { return errors.New("invalid Type") } if err := m.BlockID.ValidateBasic(); err != nil { return fmt.Errorf("wrong BlockID: %v", err) } return nil } // String returns a string representation. func (m *VoteSetMaj23Message) String() string { return fmt.Sprintf("[VSM23 %v/%02d/%v %v]", m.Height, m.Round, m.Type, m.BlockID) } // VoteSetBitsMessage is sent to communicate the bit-array of votes seen for the // BlockID. type VoteSetBitsMessage struct { Height int64 Round int32 Type tmproto.SignedMsgType BlockID types.BlockID Votes *bits.BitArray } // ValidateBasic performs basic validation. func (m *VoteSetBitsMessage) ValidateBasic() error { if m.Height < 0 { return errors.New("negative Height") } if !types.IsVoteTypeValid(m.Type) { return errors.New("invalid Type") } if err := m.BlockID.ValidateBasic(); err != nil { return fmt.Errorf("wrong BlockID: %v", err) } // NOTE: Votes.Size() can be zero if the node does not have any if m.Votes.Size() > types.MaxVotesCount { return fmt.Errorf("votes bit array is too big: %d, max: %d", m.Votes.Size(), types.MaxVotesCount) } return nil } // String returns a string representation. func (m *VoteSetBitsMessage) String() string { return fmt.Sprintf("[VSB %v/%02d/%v %v %v]", m.Height, m.Round, m.Type, m.BlockID, m.Votes) } // MsgToProto takes a consensus message type and returns the proto defined // consensus message. // // TODO: This needs to be removed, but WALToProto depends on this. func MsgToProto(msg Message) (*tmcons.Message, error) { if msg == nil { return nil, errors.New("consensus: message is nil") } var pb tmcons.Message switch msg := msg.(type) { case *NewRoundStepMessage: pb = tmcons.Message{ Sum: &tmcons.Message_NewRoundStep{ NewRoundStep: &tmcons.NewRoundStep{ Height: msg.Height, Round: msg.Round, Step: uint32(msg.Step), SecondsSinceStartTime: msg.SecondsSinceStartTime, LastCommitRound: msg.LastCommitRound, }, }, } case *NewValidBlockMessage: pbPartSetHeader := msg.BlockPartSetHeader.ToProto() pbBits := msg.BlockParts.ToProto() pb = tmcons.Message{ Sum: &tmcons.Message_NewValidBlock{ NewValidBlock: &tmcons.NewValidBlock{ Height: msg.Height, Round: msg.Round, BlockPartSetHeader: pbPartSetHeader, BlockParts: pbBits, IsCommit: msg.IsCommit, }, }, } case *ProposalMessage: pbP := msg.Proposal.ToProto() pb = tmcons.Message{ Sum: &tmcons.Message_Proposal{ Proposal: &tmcons.Proposal{ Proposal: *pbP, }, }, } case *ProposalPOLMessage: pbBits := msg.ProposalPOL.ToProto() pb = tmcons.Message{ Sum: &tmcons.Message_ProposalPol{ ProposalPol: &tmcons.ProposalPOL{ Height: msg.Height, ProposalPolRound: msg.ProposalPOLRound, ProposalPol: *pbBits, }, }, } case *BlockPartMessage: parts, err := msg.Part.ToProto() if err != nil { return nil, fmt.Errorf("msg to proto error: %w", err) } pb = tmcons.Message{ Sum: &tmcons.Message_BlockPart{ BlockPart: &tmcons.BlockPart{ Height: msg.Height, Round: msg.Round, Part: *parts, }, }, } case *VoteMessage: vote := msg.Vote.ToProto() pb = tmcons.Message{ Sum: &tmcons.Message_Vote{ Vote: &tmcons.Vote{ Vote: vote, }, }, } case *HasVoteMessage: pb = tmcons.Message{ Sum: &tmcons.Message_HasVote{ HasVote: &tmcons.HasVote{ Height: msg.Height, Round: msg.Round, Type: msg.Type, Index: msg.Index, }, }, } case *VoteSetMaj23Message: bi := msg.BlockID.ToProto() pb = tmcons.Message{ Sum: &tmcons.Message_VoteSetMaj23{ VoteSetMaj23: &tmcons.VoteSetMaj23{ Height: msg.Height, Round: msg.Round, Type: msg.Type, BlockID: bi, }, }, } case *VoteSetBitsMessage: bi := msg.BlockID.ToProto() bits := msg.Votes.ToProto() vsb := &tmcons.Message_VoteSetBits{ VoteSetBits: &tmcons.VoteSetBits{ Height: msg.Height, Round: msg.Round, Type: msg.Type, BlockID: bi, }, } if bits != nil { vsb.VoteSetBits.Votes = *bits } pb = tmcons.Message{ Sum: vsb, } default: return nil, fmt.Errorf("consensus: message not recognized: %T", msg) } return &pb, nil } // MsgFromProto takes a consensus proto message and returns the native go type. func MsgFromProto(msg *tmcons.Message) (Message, error) { if msg == nil { return nil, errors.New("consensus: nil message") } var pb Message switch msg := msg.Sum.(type) { case *tmcons.Message_NewRoundStep: rs, err := tmmath.SafeConvertUint8(int64(msg.NewRoundStep.Step)) // deny message based on possible overflow if err != nil { return nil, fmt.Errorf("denying message due to possible overflow: %w", err) } pb = &NewRoundStepMessage{ Height: msg.NewRoundStep.Height, Round: msg.NewRoundStep.Round, Step: cstypes.RoundStepType(rs), SecondsSinceStartTime: msg.NewRoundStep.SecondsSinceStartTime, LastCommitRound: msg.NewRoundStep.LastCommitRound, } case *tmcons.Message_NewValidBlock: pbPartSetHeader, err := types.PartSetHeaderFromProto(&msg.NewValidBlock.BlockPartSetHeader) if err != nil { return nil, fmt.Errorf("parts header to proto error: %w", err) } pbBits := new(bits.BitArray) err = pbBits.FromProto(msg.NewValidBlock.BlockParts) if err != nil { return nil, fmt.Errorf("parts to proto error: %w", err) } pb = &NewValidBlockMessage{ Height: msg.NewValidBlock.Height, Round: msg.NewValidBlock.Round, BlockPartSetHeader: *pbPartSetHeader, BlockParts: pbBits, IsCommit: msg.NewValidBlock.IsCommit, } case *tmcons.Message_Proposal: pbP, err := types.ProposalFromProto(&msg.Proposal.Proposal) if err != nil { return nil, fmt.Errorf("proposal msg to proto error: %w", err) } pb = &ProposalMessage{ Proposal: pbP, } case *tmcons.Message_ProposalPol: pbBits := new(bits.BitArray) err := pbBits.FromProto(&msg.ProposalPol.ProposalPol) if err != nil { return nil, fmt.Errorf("proposal PoL to proto error: %w", err) } pb = &ProposalPOLMessage{ Height: msg.ProposalPol.Height, ProposalPOLRound: msg.ProposalPol.ProposalPolRound, ProposalPOL: pbBits, } case *tmcons.Message_BlockPart: parts, err := types.PartFromProto(&msg.BlockPart.Part) if err != nil { return nil, fmt.Errorf("blockpart msg to proto error: %w", err) } pb = &BlockPartMessage{ Height: msg.BlockPart.Height, Round: msg.BlockPart.Round, Part: parts, } case *tmcons.Message_Vote: vote, err := types.VoteFromProto(msg.Vote.Vote) if err != nil { return nil, fmt.Errorf("vote msg to proto error: %w", err) } pb = &VoteMessage{ Vote: vote, } case *tmcons.Message_HasVote: pb = &HasVoteMessage{ Height: msg.HasVote.Height, Round: msg.HasVote.Round, Type: msg.HasVote.Type, Index: msg.HasVote.Index, } case *tmcons.Message_VoteSetMaj23: bi, err := types.BlockIDFromProto(&msg.VoteSetMaj23.BlockID) if err != nil { return nil, fmt.Errorf("voteSetMaj23 msg to proto error: %w", err) } pb = &VoteSetMaj23Message{ Height: msg.VoteSetMaj23.Height, Round: msg.VoteSetMaj23.Round, Type: msg.VoteSetMaj23.Type, BlockID: *bi, } case *tmcons.Message_VoteSetBits: bi, err := types.BlockIDFromProto(&msg.VoteSetBits.BlockID) if err != nil { return nil, fmt.Errorf("block ID to proto error: %w", err) } bits := new(bits.BitArray) err = bits.FromProto(&msg.VoteSetBits.Votes) if err != nil { return nil, fmt.Errorf("votes to proto error: %w", err) } pb = &VoteSetBitsMessage{ Height: msg.VoteSetBits.Height, Round: msg.VoteSetBits.Round, Type: msg.VoteSetBits.Type, BlockID: *bi, Votes: bits, } default: return nil, fmt.Errorf("consensus: message not recognized: %T", msg) } if err := pb.ValidateBasic(); err != nil { return nil, err } return pb, nil } // WALToProto takes a WAL message and return a proto walMessage and error. func WALToProto(msg WALMessage) (*tmcons.WALMessage, error) { var pb tmcons.WALMessage switch msg := msg.(type) { case types.EventDataRoundState: pb = tmcons.WALMessage{ Sum: &tmcons.WALMessage_EventDataRoundState{ EventDataRoundState: &tmproto.EventDataRoundState{ Height: msg.Height, Round: msg.Round, Step: msg.Step, }, }, } case msgInfo: consMsg, err := MsgToProto(msg.Msg) if err != nil { return nil, err } pb = tmcons.WALMessage{ Sum: &tmcons.WALMessage_MsgInfo{ MsgInfo: &tmcons.MsgInfo{ Msg: *consMsg, PeerID: string(msg.PeerID), }, }, } case timeoutInfo: pb = tmcons.WALMessage{ Sum: &tmcons.WALMessage_TimeoutInfo{ TimeoutInfo: &tmcons.TimeoutInfo{ Duration: msg.Duration, Height: msg.Height, Round: msg.Round, Step: uint32(msg.Step), }, }, } case EndHeightMessage: pb = tmcons.WALMessage{ Sum: &tmcons.WALMessage_EndHeight{ EndHeight: &tmcons.EndHeight{ Height: msg.Height, }, }, } default: return nil, fmt.Errorf("to proto: wal message not recognized: %T", msg) } return &pb, nil } // WALFromProto takes a proto wal message and return a consensus walMessage and // error. func WALFromProto(msg *tmcons.WALMessage) (WALMessage, error) { if msg == nil { return nil, errors.New("nil WAL message") } var pb WALMessage switch msg := msg.Sum.(type) { case *tmcons.WALMessage_EventDataRoundState: pb = types.EventDataRoundState{ Height: msg.EventDataRoundState.Height, Round: msg.EventDataRoundState.Round, Step: msg.EventDataRoundState.Step, } case *tmcons.WALMessage_MsgInfo: walMsg, err := MsgFromProto(&msg.MsgInfo.Msg) if err != nil { return nil, fmt.Errorf("msgInfo from proto error: %w", err) } pb = msgInfo{ Msg: walMsg, PeerID: p2p.NodeID(msg.MsgInfo.PeerID), } case *tmcons.WALMessage_TimeoutInfo: tis, err := tmmath.SafeConvertUint8(int64(msg.TimeoutInfo.Step)) // deny message based on possible overflow if err != nil { return nil, fmt.Errorf("denying message due to possible overflow: %w", err) } pb = timeoutInfo{ Duration: msg.TimeoutInfo.Duration, Height: msg.TimeoutInfo.Height, Round: msg.TimeoutInfo.Round, Step: cstypes.RoundStepType(tis), } return pb, nil case *tmcons.WALMessage_EndHeight: pb := EndHeightMessage{ Height: msg.EndHeight.Height, } return pb, nil default: return nil, fmt.Errorf("from proto: wal message not recognized: %T", msg) } return pb, nil }