Files
tendermint/types/block.go
Thane Thomson a1104b98d2 abci++: Sync implementation and spec for vote extensions (#8141)
* Refactor so building and linting works

This is the first step towards implementing vote extensions: generating
the relevant proto stubs and getting the build and linter to pass.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix typo

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Better describe method given vote extensions

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix types tests

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Move CanonicalVoteExtension to canonical types proto defs

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Regenerate protos including latest PBTS synchrony params update

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Inject vote extensions into proposal

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Thread vote extensions through code and fix tests

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove extraneous empty value initialization

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix lint

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix missing VerifyVoteExtension request data

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Explicitly ensure length > 0 to sign vote extension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Explicitly ensure length > 0 to sign vote extension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove extraneous comment

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update privval/file.go

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>

* Update types/vote_test.go

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>

* Format

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Fix ABCI proto generation scripts for Linux

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Sync intermediate and goal protos

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update internal/consensus/common_test.go

Co-authored-by: Sergio Mena <sergio@informal.systems>

* Use dummy value with clearer meaning

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Rewrite loop for clarity

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Panic on ABCI++ method call failure

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add strong correctness guarantees when constructing extended commit info for ABCI++

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add strong guarantee in extendedCommitInfo that the number of votes corresponds

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Make extendedCommitInfo function more robust

At first extendedCommitInfo expected votes to be in the same order as
their corresponding validators in the supplied CommitInfo struct, but
this proved to be rather difficult since when a validator set's loaded
from state it's first sorted by voting power and then by address.

Instead of sorting the votes in the same way, this approach simply maps
votes to their corresponding validator's address prior to constructing
the extended commit info. This way it's easy to look up the
corresponding vote and we don't need to care about vote order.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove extraneous validator address assignment

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Sign over canonical vote extension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Validate vote extension signature against canonical vote extension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update privval tests for more meaningful dummy value

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add vote extension capability to E2E test app

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Disable lint for weak RNG usage for test app

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Use parseVoteExtension instead of custom parsing in PrepareProposal

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Only include extension if we have received txs

It's unclear at this point why this is necessary to ensure that the
application's local app_hash matches that committed in the previous
block.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Require app_hash from app to match that from last block

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add contrived (possibly flaky) test to check that vote extensions code works

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove workaround for problem now solved by #8229

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* add tests for vote extension cases

* Fix spelling mistake to appease linter

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Collapse redundant if statement

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Formatting

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Always expect an extension signature, regardless of whether an extension is present

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Votes constructed from commits cannot include extensions or signatures

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Pass through vote extension in test helpers

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Temporarily disable vote extension signature requirement

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Expand on vote equality test errors for clarity

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Expand on vote matching error messages in testing

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Allow for selective subscription by vote type

This is an attempt to fix the intermittently failing
`TestPrepareProposalReceivesVoteExtensions` test in the internal
consensus package.

Occasionally we get prevote messages via the subscription channel, and
we're not interested in those. This change allows us to specify what
types of votes we're interested in (i.e. precommits) and discard the
rest.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Read lock consensus state mutex in test helper to avoid data race

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Revert BlockIDFlag parameter in node test

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Perform additional check in ProcessProposal for special txs generated by vote extensions

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* e2e: check that our added tx does not cause all txs to exceed req.MaxTxBytes

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Only set vote extension signatures when signing is successful

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove channel capacity constraint in test helper to avoid missing messages

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add TODO to always require extension signatures in vote validation

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* e2e: reject vote extensions if the request height does not match what we expect

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* types: remove extraneous call to voteWithoutExtension in test

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Remove unnecessary address parameter from CanonicalVoteExtension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* privval: change test vote type to precommit since we use an extension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* privval: update signing logic to cater for vote extensions

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* proto: update field descriptions for vote message

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* proto: update field description for vote extension sig in vote message

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* proto/types: use fixed-length 64-bit integers for rounds in CanonicalVoteExtension

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* consensus: fix flaky TestPrepareProposalReceivesVoteExtensions

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* consensus: remove previously added test helper functionality

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* e2e: add error logs when we get an unexpected height in ExtendVote or VerifyVoteExtension requests

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* node_test: get validator addresses from privvals

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* privval/file_test: optimize filepv creation in tests

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* privval: add test to check that vote extensions are always signed

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add a script to check documentation for ToC entries. (#8356)

This script verifies that each document in the docs and architecture directory
has a corresponding table-of-contents entry in its README file.  It can be run
manually from the command line.

- Hook up this script to run in CI (optional workflow).
- Update ADR ToC to include missing entries this script found.

* build(deps): Bump async from 2.6.3 to 2.6.4 in /docs (#8357)

Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* privval/file_test: reset vote ext sig before signing

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Co-authored-by: M. J. Fromberger <fromberger@interchain.io>
Co-authored-by: Sergio Mena <sergio@informal.systems>
Co-authored-by: William Banfield <wbanfield@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-19 08:05:17 -04:00

1170 lines
31 KiB
Go

package types
import (
"bytes"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/gogo/protobuf/proto"
gogotypes "github.com/gogo/protobuf/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/crypto/tmhash"
"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"
"github.com/tendermint/tendermint/version"
)
const (
// MaxHeaderBytes is a maximum header size.
// NOTE: Because app hash can be of arbitrary size, the header is therefore not
// capped in size and thus this number should be seen as a soft max
MaxHeaderBytes int64 = 626
// MaxOverheadForBlock - maximum overhead to encode a block (up to
// MaxBlockSizeBytes in size) not including it's parts except Data.
// This means it also excludes the overhead for individual transactions.
//
// Uvarint length of MaxBlockSizeBytes: 4 bytes
// 2 fields (2 embedded): 2 bytes
// Uvarint length of Data.Txs: 4 bytes
// Data.Txs field: 1 byte
MaxOverheadForBlock int64 = 11
)
// Block defines the atomic unit of a Tendermint blockchain.
type Block struct {
mtx sync.Mutex
Header `json:"header"`
Data `json:"data"`
Evidence EvidenceList `json:"evidence"`
LastCommit *Commit `json:"last_commit"`
}
// ValidateBasic performs basic validation that doesn't involve state data.
// It checks the internal consistency of the block.
// Further validation is done using state#ValidateBlock.
func (b *Block) ValidateBasic() error {
if b == nil {
return errors.New("nil block")
}
b.mtx.Lock()
defer b.mtx.Unlock()
if err := b.Header.ValidateBasic(); err != nil {
return fmt.Errorf("invalid header: %w", err)
}
// Validate the last commit and its hash.
if b.LastCommit == nil {
return errors.New("nil LastCommit")
}
if err := b.LastCommit.ValidateBasic(); err != nil {
return fmt.Errorf("wrong LastCommit: %w", err)
}
if w, g := b.LastCommit.Hash(), b.LastCommitHash; !bytes.Equal(w, g) {
return fmt.Errorf("wrong Header.LastCommitHash. Expected %X, got %X", w, g)
}
// NOTE: b.Data.Txs may be nil, but b.Data.Hash() still works fine.
if w, g := b.Data.Hash(), b.DataHash; !bytes.Equal(w, g) {
return fmt.Errorf("wrong Header.DataHash. Expected %X, got %X", w, g)
}
// NOTE: b.Evidence may be nil, but we're just looping.
for i, ev := range b.Evidence {
if err := ev.ValidateBasic(); err != nil {
return fmt.Errorf("invalid evidence (#%d): %v", i, err)
}
}
if w, g := b.Evidence.Hash(), b.EvidenceHash; !bytes.Equal(w, g) {
return fmt.Errorf("wrong Header.EvidenceHash. Expected %X, got %X", w, g)
}
return nil
}
// fillHeader fills in any remaining header fields that are a function of the block data
func (b *Block) fillHeader() {
if b.LastCommitHash == nil {
b.LastCommitHash = b.LastCommit.Hash()
}
if b.DataHash == nil {
b.DataHash = b.Data.Hash()
}
if b.EvidenceHash == nil {
b.EvidenceHash = b.Evidence.Hash()
}
}
// Hash computes and returns the block hash.
// If the block is incomplete, block hash is nil for safety.
func (b *Block) Hash() tmbytes.HexBytes {
if b == nil {
return nil
}
b.mtx.Lock()
defer b.mtx.Unlock()
if b.LastCommit == nil {
return nil
}
b.fillHeader()
return b.Header.Hash()
}
// MakePartSet returns a PartSet containing parts of a serialized block.
// This is the form in which the block is gossipped to peers.
// CONTRACT: partSize is greater than zero.
func (b *Block) MakePartSet(partSize uint32) (*PartSet, error) {
if b == nil {
return nil, errors.New("nil block")
}
b.mtx.Lock()
defer b.mtx.Unlock()
pbb, err := b.ToProto()
if err != nil {
return nil, err
}
bz, err := proto.Marshal(pbb)
if err != nil {
return nil, err
}
return NewPartSetFromData(bz, partSize), nil
}
// HashesTo is a convenience function that checks if a block hashes to the given argument.
// Returns false if the block is nil or the hash is empty.
func (b *Block) HashesTo(hash []byte) bool {
if len(hash) == 0 {
return false
}
if b == nil {
return false
}
return bytes.Equal(b.Hash(), hash)
}
// Size returns size of the block in bytes.
func (b *Block) Size() int {
pbb, err := b.ToProto()
if err != nil {
return 0
}
return pbb.Size()
}
// String returns a string representation of the block
//
// See StringIndented.
func (b *Block) String() string {
return b.StringIndented("")
}
// StringIndented returns an indented String.
//
// Header
// Data
// Evidence
// LastCommit
// Hash
func (b *Block) StringIndented(indent string) string {
if b == nil {
return "nil-Block"
}
return fmt.Sprintf(`Block{
%s %v
%s %v
%s %v
%s %v
%s}#%v`,
indent, b.Header.StringIndented(indent+" "),
indent, b.Data.StringIndented(indent+" "),
indent, b.Evidence.StringIndented(indent+" "),
indent, b.LastCommit.StringIndented(indent+" "),
indent, b.Hash())
}
// StringShort returns a shortened string representation of the block.
func (b *Block) StringShort() string {
if b == nil {
return "nil-Block"
}
return fmt.Sprintf("Block#%X", b.Hash())
}
// ToProto converts Block to protobuf
func (b *Block) ToProto() (*tmproto.Block, error) {
if b == nil {
return nil, errors.New("nil Block")
}
pb := new(tmproto.Block)
pb.Header = *b.Header.ToProto()
pb.LastCommit = b.LastCommit.ToProto()
pb.Data = b.Data.ToProto()
protoEvidence, err := b.Evidence.ToProto()
if err != nil {
return nil, err
}
pb.Evidence = *protoEvidence
return pb, nil
}
// FromProto sets a protobuf Block to the given pointer.
// It returns an error if the block is invalid.
func BlockFromProto(bp *tmproto.Block) (*Block, error) {
if bp == nil {
return nil, errors.New("nil block")
}
b := new(Block)
h, err := HeaderFromProto(&bp.Header)
if err != nil {
return nil, err
}
b.Header = h
data, err := DataFromProto(&bp.Data)
if err != nil {
return nil, err
}
b.Data = data
if err := b.Evidence.FromProto(&bp.Evidence); err != nil {
return nil, err
}
if bp.LastCommit != nil {
lc, err := CommitFromProto(bp.LastCommit)
if err != nil {
return nil, err
}
b.LastCommit = lc
}
return b, b.ValidateBasic()
}
//-----------------------------------------------------------------------------
// MaxDataBytes returns the maximum size of block's data.
//
// XXX: Panics on negative result.
func MaxDataBytes(maxBytes, evidenceBytes int64, valsCount int) int64 {
maxDataBytes := maxBytes -
MaxOverheadForBlock -
MaxHeaderBytes -
MaxCommitBytes(valsCount) -
evidenceBytes
if maxDataBytes < 0 {
panic(fmt.Sprintf(
"Negative MaxDataBytes. Block.MaxBytes=%d is too small to accommodate header&lastCommit&evidence=%d",
maxBytes,
-(maxDataBytes - maxBytes),
))
}
return maxDataBytes
}
// MaxDataBytesNoEvidence returns the maximum size of block's data when
// evidence count is unknown. MaxEvidencePerBlock will be used for the size
// of evidence.
//
// XXX: Panics on negative result.
func MaxDataBytesNoEvidence(maxBytes int64, valsCount int) int64 {
maxDataBytes := maxBytes -
MaxOverheadForBlock -
MaxHeaderBytes -
MaxCommitBytes(valsCount)
if maxDataBytes < 0 {
panic(fmt.Sprintf(
"Negative MaxDataBytesUnknownEvidence. Block.MaxBytes=%d is too small to accommodate header&lastCommit&evidence=%d",
maxBytes,
-(maxDataBytes - maxBytes),
))
}
return maxDataBytes
}
// MakeBlock returns a new block with an empty header, except what can be
// computed from itself.
// It populates the same set of fields validated by ValidateBasic.
func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block {
block := &Block{
Header: Header{
Version: version.Consensus{Block: version.BlockProtocol, App: 0},
Height: height,
},
Data: Data{
Txs: txs,
},
Evidence: evidence,
LastCommit: lastCommit,
}
block.fillHeader()
return block
}
//-----------------------------------------------------------------------------
// Header defines the structure of a Tendermint block header.
// NOTE: changes to the Header should be duplicated in:
// - header.Hash()
// - abci.Header
// - https://github.com/tendermint/tendermint/blob/master/spec/core/data_structures.md
type Header struct {
// basic block info
Version version.Consensus `json:"version"`
ChainID string `json:"chain_id"`
Height int64 `json:"height,string"`
Time time.Time `json:"time"`
// prev block info
LastBlockID BlockID `json:"last_block_id"`
// hashes of block data
LastCommitHash tmbytes.HexBytes `json:"last_commit_hash"` // commit from validators from the last block
DataHash tmbytes.HexBytes `json:"data_hash"` // transactions
// hashes from the app output from the prev block
ValidatorsHash tmbytes.HexBytes `json:"validators_hash"` // validators for the current block
NextValidatorsHash tmbytes.HexBytes `json:"next_validators_hash"` // validators for the next block
ConsensusHash tmbytes.HexBytes `json:"consensus_hash"` // consensus params for current block
AppHash tmbytes.HexBytes `json:"app_hash"` // state after txs from the previous block
// root hash of all results from the txs from the previous block
// see `deterministicResponseDeliverTx` to understand which parts of a tx is hashed into here
LastResultsHash tmbytes.HexBytes `json:"last_results_hash"`
// consensus info
EvidenceHash tmbytes.HexBytes `json:"evidence_hash"` // evidence included in the block
ProposerAddress Address `json:"proposer_address"` // original proposer of the block
}
// Populate the Header with state-derived data.
// Call this after MakeBlock to complete the Header.
func (h *Header) Populate(
version version.Consensus, chainID string,
timestamp time.Time, lastBlockID BlockID,
valHash, nextValHash []byte,
consensusHash, appHash, lastResultsHash []byte,
proposerAddress Address,
) {
h.Version = version
h.ChainID = chainID
h.Time = timestamp
h.LastBlockID = lastBlockID
h.ValidatorsHash = valHash
h.NextValidatorsHash = nextValHash
h.ConsensusHash = consensusHash
h.AppHash = appHash
h.LastResultsHash = lastResultsHash
h.ProposerAddress = proposerAddress
}
// ValidateBasic performs stateless validation on a Header returning an error
// if any validation fails.
//
// NOTE: Timestamp validation is subtle and handled elsewhere.
func (h Header) ValidateBasic() error {
if h.Version.Block != version.BlockProtocol {
return fmt.Errorf("block protocol is incorrect: got: %d, want: %d ", h.Version.Block, version.BlockProtocol)
}
if len(h.ChainID) > MaxChainIDLen {
return fmt.Errorf("chainID is too long; got: %d, max: %d", len(h.ChainID), MaxChainIDLen)
}
if h.Height < 0 {
return errors.New("negative Height")
} else if h.Height == 0 {
return errors.New("zero Height")
}
if err := h.LastBlockID.ValidateBasic(); err != nil {
return fmt.Errorf("wrong LastBlockID: %w", err)
}
if err := ValidateHash(h.LastCommitHash); err != nil {
return fmt.Errorf("wrong LastCommitHash: %w", err)
}
if err := ValidateHash(h.DataHash); err != nil {
return fmt.Errorf("wrong DataHash: %w", err)
}
if err := ValidateHash(h.EvidenceHash); err != nil {
return fmt.Errorf("wrong EvidenceHash: %w", err)
}
if len(h.ProposerAddress) != crypto.AddressSize {
return fmt.Errorf(
"invalid ProposerAddress length; got: %d, expected: %d",
len(h.ProposerAddress), crypto.AddressSize,
)
}
// Basic validation of hashes related to application data.
// Will validate fully against state in state#ValidateBlock.
if err := ValidateHash(h.ValidatorsHash); err != nil {
return fmt.Errorf("wrong ValidatorsHash: %w", err)
}
if err := ValidateHash(h.NextValidatorsHash); err != nil {
return fmt.Errorf("wrong NextValidatorsHash: %w", err)
}
if err := ValidateHash(h.ConsensusHash); err != nil {
return fmt.Errorf("wrong ConsensusHash: %w", err)
}
// NOTE: AppHash is arbitrary length
if err := ValidateHash(h.LastResultsHash); err != nil {
return fmt.Errorf("wrong LastResultsHash: %w", err)
}
return nil
}
// Hash returns the hash of the header.
// It computes a Merkle tree from the header fields
// ordered as they appear in the Header.
// Returns nil if ValidatorHash is missing,
// since a Header is not valid unless there is
// a ValidatorsHash (corresponding to the validator set).
func (h *Header) Hash() tmbytes.HexBytes {
if h == nil || len(h.ValidatorsHash) == 0 {
return nil
}
hpb := h.Version.ToProto()
hbz, err := hpb.Marshal()
if err != nil {
return nil
}
pbt, err := gogotypes.StdTimeMarshal(h.Time)
if err != nil {
return nil
}
pbbi := h.LastBlockID.ToProto()
bzbi, err := pbbi.Marshal()
if err != nil {
return nil
}
return merkle.HashFromByteSlices([][]byte{
hbz,
cdcEncode(h.ChainID),
cdcEncode(h.Height),
pbt,
bzbi,
cdcEncode(h.LastCommitHash),
cdcEncode(h.DataHash),
cdcEncode(h.ValidatorsHash),
cdcEncode(h.NextValidatorsHash),
cdcEncode(h.ConsensusHash),
cdcEncode(h.AppHash),
cdcEncode(h.LastResultsHash),
cdcEncode(h.EvidenceHash),
cdcEncode(h.ProposerAddress),
})
}
// StringIndented returns an indented string representation of the header.
func (h *Header) StringIndented(indent string) string {
if h == nil {
return "nil-Header"
}
return fmt.Sprintf(`Header{
%s Version: %v
%s ChainID: %v
%s Height: %v
%s Time: %v
%s LastBlockID: %v
%s LastCommit: %v
%s Data: %v
%s Validators: %v
%s NextValidators: %v
%s App: %v
%s Consensus: %v
%s Results: %v
%s Evidence: %v
%s Proposer: %v
%s}#%v`,
indent, h.Version,
indent, h.ChainID,
indent, h.Height,
indent, h.Time,
indent, h.LastBlockID,
indent, h.LastCommitHash,
indent, h.DataHash,
indent, h.ValidatorsHash,
indent, h.NextValidatorsHash,
indent, h.AppHash,
indent, h.ConsensusHash,
indent, h.LastResultsHash,
indent, h.EvidenceHash,
indent, h.ProposerAddress,
indent, h.Hash(),
)
}
// ToProto converts Header to protobuf
func (h *Header) ToProto() *tmproto.Header {
if h == nil {
return nil
}
return &tmproto.Header{
Version: h.Version.ToProto(),
ChainID: h.ChainID,
Height: h.Height,
Time: h.Time,
LastBlockId: h.LastBlockID.ToProto(),
ValidatorsHash: h.ValidatorsHash,
NextValidatorsHash: h.NextValidatorsHash,
ConsensusHash: h.ConsensusHash,
AppHash: h.AppHash,
DataHash: h.DataHash,
EvidenceHash: h.EvidenceHash,
LastResultsHash: h.LastResultsHash,
LastCommitHash: h.LastCommitHash,
ProposerAddress: h.ProposerAddress,
}
}
// FromProto sets a protobuf Header to the given pointer.
// It returns an error if the header is invalid.
func HeaderFromProto(ph *tmproto.Header) (Header, error) {
if ph == nil {
return Header{}, errors.New("nil Header")
}
h := new(Header)
bi, err := BlockIDFromProto(&ph.LastBlockId)
if err != nil {
return Header{}, err
}
h.Version = version.Consensus{Block: ph.Version.Block, App: ph.Version.App}
h.ChainID = ph.ChainID
h.Height = ph.Height
h.Time = ph.Time
h.Height = ph.Height
h.LastBlockID = *bi
h.ValidatorsHash = ph.ValidatorsHash
h.NextValidatorsHash = ph.NextValidatorsHash
h.ConsensusHash = ph.ConsensusHash
h.AppHash = ph.AppHash
h.DataHash = ph.DataHash
h.EvidenceHash = ph.EvidenceHash
h.LastResultsHash = ph.LastResultsHash
h.LastCommitHash = ph.LastCommitHash
h.ProposerAddress = ph.ProposerAddress
return *h, h.ValidateBasic()
}
//-------------------------------------
// BlockIDFlag indicates which BlockID the signature is for.
type BlockIDFlag byte
const (
// BlockIDFlagAbsent - no vote was received from a validator.
BlockIDFlagAbsent BlockIDFlag = iota + 1
// BlockIDFlagCommit - voted for the Commit.BlockID.
BlockIDFlagCommit
// BlockIDFlagNil - voted for nil.
BlockIDFlagNil
)
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 Address `json:"validator_address"`
Timestamp time.Time `json:"timestamp"`
Signature []byte `json:"signature"`
}
// NewCommitSigForBlock returns new CommitSig with BlockIDFlagCommit.
func NewCommitSigForBlock(signature []byte, valAddr 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,string"`
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,
}
}
// CommitToVoteSet constructs a VoteSet from the Commit and validator set.
// Panics if signatures from the commit can't be added to the voteset.
// Inverse of VoteSet.MakeCommit().
func CommitToVoteSet(chainID string, commit *Commit, vals *ValidatorSet) *VoteSet {
voteSet := NewVoteSet(chainID, commit.Height, commit.Round, tmproto.PrecommitType, vals)
for idx, commitSig := range commit.Signatures {
if commitSig.Absent() {
continue // OK, some precommits can be missing.
}
added, err := voteSet.AddVote(commit.GetVote(int32(idx)))
if !added || err != nil {
panic(fmt.Errorf("failed to reconstruct LastCommit: %w", err))
}
}
return voteSet
}
// GetVote converts the CommitSig for the given valIdx to a Vote.
// Returns nil if the precommit at valIdx is nil.
// Panics if valIdx >= commit.Size().
func (commit *Commit) GetVote(valIdx int32) *Vote {
commitSig := commit.Signatures[valIdx]
return &Vote{
Type: tmproto.PrecommitType,
Height: commit.Height,
Round: commit.Round,
BlockID: commitSig.BlockID(commit.BlockID),
Timestamp: commitSig.Timestamp,
ValidatorAddress: commitSig.ValidatorAddress,
ValidatorIndex: valIdx,
Signature: commitSig.Signature,
}
}
// VoteSignBytes returns the bytes of the Vote corresponding to valIdx for
// signing.
//
// The only unique part is the Timestamp - all other fields signed over are
// otherwise the same for all validators.
//
// Panics if valIdx >= commit.Size().
//
// See VoteSignBytes
func (commit *Commit) VoteSignBytes(chainID string, valIdx int32) []byte {
v := commit.GetVote(valIdx).ToProto()
return VoteSignBytes(chainID, v)
}
// 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)
}
// 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
}
// GetByIndex returns the vote corresponding to a given validator index.
// Panics if `index >= commit.Size()`.
// Implements VoteSetReader.
func (commit *Commit) GetByIndex(valIdx int32) *Vote {
return commit.GetVote(valIdx)
}
// 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.IsNil() {
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()
}
//-----------------------------------------------------------------------------
// Data contains the set of transactions included in the block
type Data struct {
// Txs that will be applied by state @ block.Height+1.
// NOTE: not all txs here are valid. We're just agreeing on the order first.
// This means that block.AppHash does not include these txs.
Txs Txs `json:"txs"`
// Volatile
hash tmbytes.HexBytes
}
// Hash returns the hash of the data
func (data *Data) Hash() tmbytes.HexBytes {
if data == nil {
return (Txs{}).Hash()
}
if data.hash == nil {
data.hash = data.Txs.Hash() // NOTE: leaves of merkle tree are TxIDs
}
return data.hash
}
// StringIndented returns an indented string representation of the transactions.
func (data *Data) StringIndented(indent string) string {
if data == nil {
return "nil-Data"
}
txStrings := make([]string, tmmath.MinInt(len(data.Txs), 21))
for i, tx := range data.Txs {
if i == 20 {
txStrings[i] = fmt.Sprintf("... (%v total)", len(data.Txs))
break
}
txStrings[i] = fmt.Sprintf("%X (%d bytes)", tx.Hash(), len(tx))
}
return fmt.Sprintf(`Data{
%s %v
%s}#%v`,
indent, strings.Join(txStrings, "\n"+indent+" "),
indent, data.hash)
}
// ToProto converts Data to protobuf
func (data *Data) ToProto() tmproto.Data {
tp := new(tmproto.Data)
if len(data.Txs) > 0 {
txBzs := make([][]byte, len(data.Txs))
for i := range data.Txs {
txBzs[i] = data.Txs[i]
}
tp.Txs = txBzs
}
return *tp
}
// DataFromProto takes a protobuf representation of Data &
// returns the native type.
func DataFromProto(dp *tmproto.Data) (Data, error) {
if dp == nil {
return Data{}, errors.New("nil data")
}
data := new(Data)
if len(dp.Txs) > 0 {
txBzs := make(Txs, len(dp.Txs))
for i := range dp.Txs {
txBzs[i] = Tx(dp.Txs[i])
}
data.Txs = txBzs
} else {
data.Txs = Txs{}
}
return *data, nil
}
//--------------------------------------------------------------------------------
// BlockID
type BlockID struct {
Hash tmbytes.HexBytes `json:"hash"`
PartSetHeader PartSetHeader `json:"parts"`
}
// Equals returns true if the BlockID matches the given BlockID
func (blockID BlockID) Equals(other BlockID) bool {
return bytes.Equal(blockID.Hash, other.Hash) &&
blockID.PartSetHeader.Equals(other.PartSetHeader)
}
// Key returns a machine-readable string representation of the BlockID
func (blockID BlockID) Key() string {
pbph := blockID.PartSetHeader.ToProto()
bz, err := pbph.Marshal()
if err != nil {
panic(err)
}
return fmt.Sprint(string(blockID.Hash), string(bz))
}
// ValidateBasic performs basic validation.
func (blockID BlockID) ValidateBasic() error {
// Hash can be empty in case of POLBlockID in Proposal.
if err := ValidateHash(blockID.Hash); err != nil {
return fmt.Errorf("wrong Hash: %w", err)
}
if err := blockID.PartSetHeader.ValidateBasic(); err != nil {
return fmt.Errorf("wrong PartSetHeader: %w", err)
}
return nil
}
// IsNil returns true if this is the BlockID of a nil block.
func (blockID BlockID) IsNil() bool {
return len(blockID.Hash) == 0 &&
blockID.PartSetHeader.IsZero()
}
// IsComplete returns true if this is a valid BlockID of a non-nil block.
func (blockID BlockID) IsComplete() bool {
return len(blockID.Hash) == tmhash.Size &&
blockID.PartSetHeader.Total > 0 &&
len(blockID.PartSetHeader.Hash) == tmhash.Size
}
// String returns a human readable string representation of the BlockID.
//
// 1. hash
// 2. part set header
//
// See PartSetHeader#String
func (blockID BlockID) String() string {
return fmt.Sprintf(`%v:%v`, blockID.Hash, blockID.PartSetHeader)
}
// ToProto converts BlockID to protobuf
func (blockID *BlockID) ToProto() tmproto.BlockID {
if blockID == nil {
return tmproto.BlockID{}
}
return tmproto.BlockID{
Hash: blockID.Hash,
PartSetHeader: blockID.PartSetHeader.ToProto(),
}
}
// FromProto sets a protobuf BlockID to the given pointer.
// It returns an error if the block id is invalid.
func BlockIDFromProto(bID *tmproto.BlockID) (*BlockID, error) {
if bID == nil {
return nil, errors.New("nil BlockID")
}
blockID := new(BlockID)
ph, err := PartSetHeaderFromProto(&bID.PartSetHeader)
if err != nil {
return nil, err
}
blockID.PartSetHeader = *ph
blockID.Hash = bID.Hash
return blockID, blockID.ValidateBasic()
}