package metadata import ( "crypto/ed25519" "errors" "fmt" "strings" "time" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/merkle" "github.com/tendermint/tendermint/libs/bits" tmbytes "github.com/tendermint/tendermint/libs/bytes" tmmath "github.com/tendermint/tendermint/libs/math" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) var ( // MaxSignatureSize is a maximum allowed signature size for the Proposal // and Vote. // XXX: secp256k1 does not have Size nor MaxSize defined. MaxSignatureSize = tmmath.MaxInt(ed25519.SignatureSize, 64) ) //------------------------------------- const ( // Max size of commit without any commitSigs -> 82 for BlockID, 8 for Height, 4 for Round. MaxCommitOverheadBytes int64 = 94 // Commit sig size is made up of 64 bytes for the signature, 20 bytes for the address, // 1 byte for the flag and 14 bytes for the timestamp MaxCommitSigBytes int64 = 109 ) // CommitSig is a part of the Vote included in a Commit. type CommitSig struct { BlockIDFlag BlockIDFlag `json:"block_id_flag"` ValidatorAddress crypto.Address `json:"validator_address"` Timestamp time.Time `json:"timestamp"` Signature []byte `json:"signature"` } // NewCommitSigForBlock returns new CommitSig with BlockIDFlagCommit. func NewCommitSigForBlock(signature []byte, valAddr crypto.Address, ts time.Time) CommitSig { return CommitSig{ BlockIDFlag: BlockIDFlagCommit, ValidatorAddress: valAddr, Timestamp: ts, Signature: signature, } } func MaxCommitBytes(valCount int) int64 { // From the repeated commit sig field var protoEncodingOverhead int64 = 2 return MaxCommitOverheadBytes + ((MaxCommitSigBytes + protoEncodingOverhead) * int64(valCount)) } // NewCommitSigAbsent returns new CommitSig with BlockIDFlagAbsent. Other // fields are all empty. func NewCommitSigAbsent() CommitSig { return CommitSig{ BlockIDFlag: BlockIDFlagAbsent, } } // ForBlock returns true if CommitSig is for the block. func (cs CommitSig) ForBlock() bool { return cs.BlockIDFlag == BlockIDFlagCommit } // Absent returns true if CommitSig is absent. func (cs CommitSig) Absent() bool { return cs.BlockIDFlag == BlockIDFlagAbsent } // CommitSig returns a string representation of CommitSig. // // 1. first 6 bytes of signature // 2. first 6 bytes of validator address // 3. block ID flag // 4. timestamp func (cs CommitSig) String() string { return fmt.Sprintf("CommitSig{%X by %X on %v @ %s}", tmbytes.Fingerprint(cs.Signature), tmbytes.Fingerprint(cs.ValidatorAddress), cs.BlockIDFlag, CanonicalTime(cs.Timestamp)) } // BlockID returns the Commit's BlockID if CommitSig indicates signing, // otherwise - empty BlockID. func (cs CommitSig) BlockID(commitBlockID BlockID) BlockID { var blockID BlockID switch cs.BlockIDFlag { case BlockIDFlagAbsent: blockID = BlockID{} case BlockIDFlagCommit: blockID = commitBlockID case BlockIDFlagNil: blockID = BlockID{} default: panic(fmt.Sprintf("Unknown BlockIDFlag: %v", cs.BlockIDFlag)) } return blockID } // ValidateBasic performs basic validation. func (cs CommitSig) ValidateBasic() error { switch cs.BlockIDFlag { case BlockIDFlagAbsent: case BlockIDFlagCommit: case BlockIDFlagNil: default: return fmt.Errorf("unknown BlockIDFlag: %v", cs.BlockIDFlag) } switch cs.BlockIDFlag { case BlockIDFlagAbsent: if len(cs.ValidatorAddress) != 0 { return errors.New("validator address is present") } if !cs.Timestamp.IsZero() { return errors.New("time is present") } if len(cs.Signature) != 0 { return errors.New("signature is present") } default: if len(cs.ValidatorAddress) != crypto.AddressSize { return fmt.Errorf("expected ValidatorAddress size to be %d bytes, got %d bytes", crypto.AddressSize, len(cs.ValidatorAddress), ) } // NOTE: Timestamp validation is subtle and handled elsewhere. if len(cs.Signature) == 0 { return errors.New("signature is missing") } if len(cs.Signature) > MaxSignatureSize { return fmt.Errorf("signature is too big (max: %d)", MaxSignatureSize) } } return nil } // ToProto converts CommitSig to protobuf func (cs *CommitSig) ToProto() *tmproto.CommitSig { if cs == nil { return nil } return &tmproto.CommitSig{ BlockIdFlag: tmproto.BlockIDFlag(cs.BlockIDFlag), ValidatorAddress: cs.ValidatorAddress, Timestamp: cs.Timestamp, Signature: cs.Signature, } } // FromProto sets a protobuf CommitSig to the given pointer. // It returns an error if the CommitSig is invalid. func (cs *CommitSig) FromProto(csp tmproto.CommitSig) error { cs.BlockIDFlag = BlockIDFlag(csp.BlockIdFlag) cs.ValidatorAddress = csp.ValidatorAddress cs.Timestamp = csp.Timestamp cs.Signature = csp.Signature return cs.ValidateBasic() } //------------------------------------- // Commit contains the evidence that a block was committed by a set of validators. // NOTE: Commit is empty for height 1, but never nil. type Commit struct { // NOTE: The signatures are in order of address to preserve the bonded // ValidatorSet order. // Any peer with a block can gossip signatures by index with a peer without // recalculating the active ValidatorSet. Height int64 `json:"height"` Round int32 `json:"round"` BlockID BlockID `json:"block_id"` Signatures []CommitSig `json:"signatures"` // Memoized in first call to corresponding method. // NOTE: can't memoize in constructor because constructor isn't used for // unmarshaling. hash tmbytes.HexBytes bitArray *bits.BitArray } // NewCommit returns a new Commit. func NewCommit(height int64, round int32, blockID BlockID, commitSigs []CommitSig) *Commit { return &Commit{ Height: height, Round: round, BlockID: blockID, Signatures: commitSigs, } } // Type returns the vote type of the commit, which is always VoteTypePrecommit // Implements VoteSetReader. func (commit *Commit) Type() byte { return byte(tmproto.PrecommitType) } // GetHeight returns height of the commit. // Implements VoteSetReader. func (commit *Commit) GetHeight() int64 { return commit.Height } // GetRound returns height of the commit. // Implements VoteSetReader. func (commit *Commit) GetRound() int32 { return commit.Round } // Size returns the number of signatures in the commit. // Implements VoteSetReader. func (commit *Commit) Size() int { if commit == nil { return 0 } return len(commit.Signatures) } // ClearCache removes the saved hash. This is predominantly used for testing. func (commit *Commit) ClearCache() { commit.hash = nil commit.bitArray = nil } // BitArray returns a BitArray of which validators voted for BlockID or nil in this commit. // Implements VoteSetReader. func (commit *Commit) BitArray() *bits.BitArray { if commit.bitArray == nil { commit.bitArray = bits.NewBitArray(len(commit.Signatures)) for i, commitSig := range commit.Signatures { // TODO: need to check the BlockID otherwise we could be counting conflicts, // not just the one with +2/3 ! commit.bitArray.SetIndex(i, !commitSig.Absent()) } } return commit.bitArray } // IsCommit returns true if there is at least one signature. // Implements VoteSetReader. func (commit *Commit) IsCommit() bool { return len(commit.Signatures) != 0 } // ValidateBasic performs basic validation that doesn't involve state data. // Does not actually check the cryptographic signatures. func (commit *Commit) ValidateBasic() error { if commit.Height < 0 { return errors.New("negative Height") } if commit.Round < 0 { return errors.New("negative Round") } if commit.Height >= 1 { if commit.BlockID.IsZero() { return errors.New("commit cannot be for nil block") } if len(commit.Signatures) == 0 { return errors.New("no signatures in commit") } for i, commitSig := range commit.Signatures { if err := commitSig.ValidateBasic(); err != nil { return fmt.Errorf("wrong CommitSig #%d: %v", i, err) } } } return nil } // Hash returns the hash of the commit func (commit *Commit) Hash() tmbytes.HexBytes { if commit == nil { return nil } if commit.hash == nil { bs := make([][]byte, len(commit.Signatures)) for i, commitSig := range commit.Signatures { pbcs := commitSig.ToProto() bz, err := pbcs.Marshal() if err != nil { panic(err) } bs[i] = bz } commit.hash = merkle.HashFromByteSlices(bs) } return commit.hash } // StringIndented returns a string representation of the commit. func (commit *Commit) StringIndented(indent string) string { if commit == nil { return "nil-Commit" } commitSigStrings := make([]string, len(commit.Signatures)) for i, commitSig := range commit.Signatures { commitSigStrings[i] = commitSig.String() } return fmt.Sprintf(`Commit{ %s Height: %d %s Round: %d %s BlockID: %v %s Signatures: %s %v %s}#%v`, indent, commit.Height, indent, commit.Round, indent, commit.BlockID, indent, indent, strings.Join(commitSigStrings, "\n"+indent+" "), indent, commit.hash) } // ToProto converts Commit to protobuf func (commit *Commit) ToProto() *tmproto.Commit { if commit == nil { return nil } c := new(tmproto.Commit) sigs := make([]tmproto.CommitSig, len(commit.Signatures)) for i := range commit.Signatures { sigs[i] = *commit.Signatures[i].ToProto() } c.Signatures = sigs c.Height = commit.Height c.Round = commit.Round c.BlockID = commit.BlockID.ToProto() return c } // FromProto sets a protobuf Commit to the given pointer. // It returns an error if the commit is invalid. func CommitFromProto(cp *tmproto.Commit) (*Commit, error) { if cp == nil { return nil, errors.New("nil Commit") } var ( commit = new(Commit) ) bi, err := BlockIDFromProto(&cp.BlockID) if err != nil { return nil, err } sigs := make([]CommitSig, len(cp.Signatures)) for i := range cp.Signatures { if err := sigs[i].FromProto(cp.Signatures[i]); err != nil { return nil, err } } commit.Signatures = sigs commit.Height = cp.Height commit.Round = cp.Round commit.BlockID = *bi return commit, commit.ValidateBasic() }