mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-09 09:36:05 +00:00
rename to metadata
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
// Canonical* wraps the structs in types for amino encoding them for use in SignBytes / the Signable interface.
|
||||
|
||||
// TimeFormat is used for generating the sigs
|
||||
const TimeFormat = time.RFC3339Nano
|
||||
|
||||
//-----------------------------------
|
||||
// Canonicalize the structs
|
||||
|
||||
func CanonicalizeBlockID(bid tmproto.BlockID) *tmproto.CanonicalBlockID {
|
||||
rbid, err := BlockIDFromProto(&bid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var cbid *tmproto.CanonicalBlockID
|
||||
if rbid == nil || rbid.IsZero() {
|
||||
cbid = nil
|
||||
} else {
|
||||
cbid = &tmproto.CanonicalBlockID{
|
||||
Hash: bid.Hash,
|
||||
PartSetHeader: CanonicalizePartSetHeader(bid.PartSetHeader),
|
||||
}
|
||||
}
|
||||
|
||||
return cbid
|
||||
}
|
||||
|
||||
// CanonicalizeVote transforms the given PartSetHeader to a CanonicalPartSetHeader.
|
||||
func CanonicalizePartSetHeader(psh tmproto.PartSetHeader) tmproto.CanonicalPartSetHeader {
|
||||
return tmproto.CanonicalPartSetHeader(psh)
|
||||
}
|
||||
|
||||
// CanonicalTime can be used to stringify time in a canonical way.
|
||||
func CanonicalTime(t time.Time) string {
|
||||
// Note that sending time over amino resets it to
|
||||
// local time, we need to force UTC here, so the
|
||||
// signatures match
|
||||
return tmtime.Canonical(t).Format(TimeFormat)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package metadata_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"github.com/tendermint/tendermint/pkg/metadata"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
func TestCanonicalizeBlockID(t *testing.T) {
|
||||
randhash := tmrand.Bytes(tmhash.Size)
|
||||
block1 := tmproto.BlockID{Hash: randhash,
|
||||
PartSetHeader: tmproto.PartSetHeader{Total: 5, Hash: randhash}}
|
||||
block2 := tmproto.BlockID{Hash: randhash,
|
||||
PartSetHeader: tmproto.PartSetHeader{Total: 10, Hash: randhash}}
|
||||
cblock1 := tmproto.CanonicalBlockID{Hash: randhash,
|
||||
PartSetHeader: tmproto.CanonicalPartSetHeader{Total: 5, Hash: randhash}}
|
||||
cblock2 := tmproto.CanonicalBlockID{Hash: randhash,
|
||||
PartSetHeader: tmproto.CanonicalPartSetHeader{Total: 10, Hash: randhash}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args tmproto.BlockID
|
||||
want *tmproto.CanonicalBlockID
|
||||
}{
|
||||
{"first", block1, &cblock1},
|
||||
{"second", block2, &cblock2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := metadata.CanonicalizeBlockID(tt.args); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CanonicalizeBlockID() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package metadata_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
test "github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/bits"
|
||||
"github.com/tendermint/tendermint/pkg/consensus"
|
||||
"github.com/tendermint/tendermint/pkg/metadata"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
func TestCommit(t *testing.T) {
|
||||
lastID := test.MakeBlockID()
|
||||
h := int64(3)
|
||||
voteSet, _, vals := test.RandVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
|
||||
commit, err := test.MakeCommit(lastID, h-1, 1, voteSet, vals, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, h-1, commit.Height)
|
||||
assert.EqualValues(t, 1, commit.Round)
|
||||
assert.Equal(t, tmproto.PrecommitType, tmproto.SignedMsgType(commit.Type()))
|
||||
if commit.Size() <= 0 {
|
||||
t.Fatalf("commit %v has a zero or negative size: %d", commit, commit.Size())
|
||||
}
|
||||
|
||||
require.NotNil(t, commit.BitArray())
|
||||
assert.Equal(t, bits.NewBitArray(10).Size(), commit.BitArray().Size())
|
||||
|
||||
assert.Equal(t, voteSet.GetByIndex(0), consensus.GetVoteFromCommit(commit, 0))
|
||||
assert.True(t, commit.IsCommit())
|
||||
}
|
||||
|
||||
func TestCommitValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
testName string
|
||||
malleateCommit func(*metadata.Commit)
|
||||
expectErr bool
|
||||
}{
|
||||
{"Random Commit", func(com *metadata.Commit) {}, false},
|
||||
{"Incorrect signature", func(com *metadata.Commit) { com.Signatures[0].Signature = []byte{0} }, false},
|
||||
{"Incorrect height", func(com *metadata.Commit) { com.Height = int64(-100) }, true},
|
||||
{"Incorrect round", func(com *metadata.Commit) { com.Round = -100 }, true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
com := test.MakeRandomCommit(time.Now())
|
||||
tc.malleateCommit(com)
|
||||
assert.Equal(t, tc.expectErr, com.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommit_ValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
commit *metadata.Commit
|
||||
expectErr bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
"invalid height",
|
||||
&metadata.Commit{Height: -1},
|
||||
true, "negative Height",
|
||||
},
|
||||
{
|
||||
"invalid round",
|
||||
&metadata.Commit{Height: 1, Round: -1},
|
||||
true, "negative Round",
|
||||
},
|
||||
{
|
||||
"invalid block ID",
|
||||
&metadata.Commit{
|
||||
Height: 1,
|
||||
Round: 1,
|
||||
BlockID: metadata.BlockID{},
|
||||
},
|
||||
true, "commit cannot be for nil block",
|
||||
},
|
||||
{
|
||||
"no signatures",
|
||||
&metadata.Commit{
|
||||
Height: 1,
|
||||
Round: 1,
|
||||
BlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
},
|
||||
true, "no signatures in commit",
|
||||
},
|
||||
{
|
||||
"invalid signature",
|
||||
&metadata.Commit{
|
||||
Height: 1,
|
||||
Round: 1,
|
||||
BlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
Signatures: []metadata.CommitSig{
|
||||
{
|
||||
BlockIDFlag: metadata.BlockIDFlagCommit,
|
||||
ValidatorAddress: make([]byte, crypto.AddressSize),
|
||||
Signature: make([]byte, metadata.MaxSignatureSize+1),
|
||||
},
|
||||
},
|
||||
},
|
||||
true, "wrong CommitSig",
|
||||
},
|
||||
{
|
||||
"valid commit",
|
||||
&metadata.Commit{
|
||||
Height: 1,
|
||||
Round: 1,
|
||||
BlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
Signatures: []metadata.CommitSig{
|
||||
{
|
||||
BlockIDFlag: metadata.BlockIDFlagCommit,
|
||||
ValidatorAddress: make([]byte, crypto.AddressSize),
|
||||
Signature: make([]byte, metadata.MaxSignatureSize),
|
||||
},
|
||||
},
|
||||
},
|
||||
false, "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.commit.ValidateBasic()
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.errString)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxCommitBytes(t *testing.T) {
|
||||
// time is varint encoded so need to pick the max.
|
||||
// year int, month Month, day, hour, min, sec, nsec int, loc *Location
|
||||
timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC)
|
||||
|
||||
cs := metadata.CommitSig{
|
||||
BlockIDFlag: metadata.BlockIDFlagNil,
|
||||
ValidatorAddress: crypto.AddressHash([]byte("validator_address")),
|
||||
Timestamp: timestamp,
|
||||
Signature: crypto.CRandBytes(metadata.MaxSignatureSize),
|
||||
}
|
||||
|
||||
pbSig := cs.ToProto()
|
||||
// test that a single commit sig doesn't exceed max commit sig bytes
|
||||
assert.EqualValues(t, metadata.MaxCommitSigBytes, pbSig.Size())
|
||||
|
||||
// check size with a single commit
|
||||
commit := &metadata.Commit{
|
||||
Height: math.MaxInt64,
|
||||
Round: math.MaxInt32,
|
||||
BlockID: metadata.BlockID{
|
||||
Hash: tmhash.Sum([]byte("blockID_hash")),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Total: math.MaxInt32,
|
||||
Hash: tmhash.Sum([]byte("blockID_part_set_header_hash")),
|
||||
},
|
||||
},
|
||||
Signatures: []metadata.CommitSig{cs},
|
||||
}
|
||||
|
||||
pb := commit.ToProto()
|
||||
|
||||
assert.EqualValues(t, metadata.MaxCommitBytes(1), int64(pb.Size()))
|
||||
|
||||
// check the upper bound of the commit size
|
||||
for i := 1; i < consensus.MaxVotesCount; i++ {
|
||||
commit.Signatures = append(commit.Signatures, cs)
|
||||
}
|
||||
|
||||
pb = commit.ToProto()
|
||||
|
||||
assert.EqualValues(t, metadata.MaxCommitBytes(consensus.MaxVotesCount), int64(pb.Size()))
|
||||
}
|
||||
|
||||
func TestCommitSig_ValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cs metadata.CommitSig
|
||||
expectErr bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
"invalid ID flag",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlag(0xFF)},
|
||||
true, "unknown BlockIDFlag",
|
||||
},
|
||||
{
|
||||
"BlockIDFlagAbsent validator address present",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlagAbsent, ValidatorAddress: crypto.Address("testaddr")},
|
||||
true, "validator address is present",
|
||||
},
|
||||
{
|
||||
"BlockIDFlagAbsent timestamp present",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlagAbsent, Timestamp: time.Now().UTC()},
|
||||
true, "time is present",
|
||||
},
|
||||
{
|
||||
"BlockIDFlagAbsent signatures present",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlagAbsent, Signature: []byte{0xAA}},
|
||||
true, "signature is present",
|
||||
},
|
||||
{
|
||||
"BlockIDFlagAbsent valid BlockIDFlagAbsent",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlagAbsent},
|
||||
false, "",
|
||||
},
|
||||
{
|
||||
"non-BlockIDFlagAbsent invalid validator address",
|
||||
metadata.CommitSig{BlockIDFlag: metadata.BlockIDFlagCommit, ValidatorAddress: make([]byte, 1)},
|
||||
true, "expected ValidatorAddress size",
|
||||
},
|
||||
{
|
||||
"non-BlockIDFlagAbsent invalid signature (zero)",
|
||||
metadata.CommitSig{
|
||||
BlockIDFlag: metadata.BlockIDFlagCommit,
|
||||
ValidatorAddress: make([]byte, crypto.AddressSize),
|
||||
Signature: make([]byte, 0),
|
||||
},
|
||||
true, "signature is missing",
|
||||
},
|
||||
{
|
||||
"non-BlockIDFlagAbsent invalid signature (too large)",
|
||||
metadata.CommitSig{
|
||||
BlockIDFlag: metadata.BlockIDFlagCommit,
|
||||
ValidatorAddress: make([]byte, crypto.AddressSize),
|
||||
Signature: make([]byte, metadata.MaxSignatureSize+1),
|
||||
},
|
||||
true, "signature is too big",
|
||||
},
|
||||
{
|
||||
"non-BlockIDFlagAbsent valid",
|
||||
metadata.CommitSig{
|
||||
BlockIDFlag: metadata.BlockIDFlagCommit,
|
||||
ValidatorAddress: make([]byte, crypto.AddressSize),
|
||||
Signature: make([]byte, metadata.MaxSignatureSize),
|
||||
},
|
||||
false, "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.cs.ValidateBasic()
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.errString)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
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
|
||||
|
||||
// MaxChainIDLen is a maximum length of the chain ID.
|
||||
MaxChainIDLen = 50
|
||||
|
||||
// MaxBlockSizeBytes is the maximum permitted size of the blocks.
|
||||
MaxBlockSizeBytes = 104857600 // 100MB
|
||||
)
|
||||
|
||||
// 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/spec/blob/master/spec/blockchain/blockchain.md
|
||||
type Header struct {
|
||||
// basic block info
|
||||
Version version.Consensus `json:"version"`
|
||||
ChainID string `json:"chain_id"`
|
||||
Height int64 `json:"height"`
|
||||
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 crypto.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 crypto.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: %v", err)
|
||||
}
|
||||
|
||||
if err := ValidateHash(h.DataHash); err != nil {
|
||||
return fmt.Errorf("wrong DataHash: %v", err)
|
||||
}
|
||||
|
||||
if err := ValidateHash(h.EvidenceHash); err != nil {
|
||||
return fmt.Errorf("wrong EvidenceHash: %v", 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: %v", err)
|
||||
}
|
||||
if err := ValidateHash(h.NextValidatorsHash); err != nil {
|
||||
return fmt.Errorf("wrong NextValidatorsHash: %v", err)
|
||||
}
|
||||
if err := ValidateHash(h.ConsensusHash); err != nil {
|
||||
return fmt.Errorf("wrong ConsensusHash: %v", err)
|
||||
}
|
||||
// NOTE: AppHash is arbitrary length
|
||||
if err := ValidateHash(h.LastResultsHash); err != nil {
|
||||
return fmt.Errorf("wrong LastResultsHash: %v", 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()
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
@@ -0,0 +1,496 @@
|
||||
package metadata_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
test "github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/bytes"
|
||||
"github.com/tendermint/tendermint/pkg/metadata"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
tmversion "github.com/tendermint/tendermint/proto/tendermint/version"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
)
|
||||
|
||||
var nilBytes []byte
|
||||
|
||||
func TestNilHeaderHashDoesntCrash(t *testing.T) {
|
||||
assert.Equal(t, nilBytes, []byte((*metadata.Header)(nil).Hash()))
|
||||
assert.Equal(t, nilBytes, []byte((new(metadata.Header)).Hash()))
|
||||
}
|
||||
|
||||
func TestHeaderHash(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
header *metadata.Header
|
||||
expectHash bytes.HexBytes
|
||||
}{
|
||||
{"Generates expected hash", &metadata.Header{
|
||||
Version: version.Consensus{Block: 1, App: 2},
|
||||
ChainID: "chainId",
|
||||
Height: 3,
|
||||
Time: time.Date(2019, 10, 13, 16, 14, 44, 0, time.UTC),
|
||||
LastBlockID: test.MakeBlockIDWithHash(make([]byte, tmhash.Size)),
|
||||
LastCommitHash: tmhash.Sum([]byte("last_commit_hash")),
|
||||
DataHash: tmhash.Sum([]byte("data_hash")),
|
||||
ValidatorsHash: tmhash.Sum([]byte("validators_hash")),
|
||||
NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")),
|
||||
ConsensusHash: tmhash.Sum([]byte("consensus_hash")),
|
||||
AppHash: tmhash.Sum([]byte("app_hash")),
|
||||
LastResultsHash: tmhash.Sum([]byte("last_results_hash")),
|
||||
EvidenceHash: tmhash.Sum([]byte("evidence_hash")),
|
||||
ProposerAddress: crypto.AddressHash([]byte("proposer_address")),
|
||||
}, hexBytesFromString("F740121F553B5418C3EFBD343C2DBFE9E007BB67B0D020A0741374BAB65242A4")},
|
||||
{"nil header yields nil", nil, nil},
|
||||
{"nil ValidatorsHash yields nil", &metadata.Header{
|
||||
Version: version.Consensus{Block: 1, App: 2},
|
||||
ChainID: "chainId",
|
||||
Height: 3,
|
||||
Time: time.Date(2019, 10, 13, 16, 14, 44, 0, time.UTC),
|
||||
LastBlockID: test.MakeBlockIDWithHash(make([]byte, tmhash.Size)),
|
||||
LastCommitHash: tmhash.Sum([]byte("last_commit_hash")),
|
||||
DataHash: tmhash.Sum([]byte("data_hash")),
|
||||
ValidatorsHash: nil,
|
||||
NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")),
|
||||
ConsensusHash: tmhash.Sum([]byte("consensus_hash")),
|
||||
AppHash: tmhash.Sum([]byte("app_hash")),
|
||||
LastResultsHash: tmhash.Sum([]byte("last_results_hash")),
|
||||
EvidenceHash: tmhash.Sum([]byte("evidence_hash")),
|
||||
ProposerAddress: crypto.AddressHash([]byte("proposer_address")),
|
||||
}, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expectHash, tc.header.Hash())
|
||||
|
||||
// We also make sure that all fields are hashed in struct order, and that all
|
||||
// fields in the test struct are non-zero.
|
||||
if tc.header != nil && tc.expectHash != nil {
|
||||
byteSlices := [][]byte{}
|
||||
|
||||
s := reflect.ValueOf(*tc.header)
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
f := s.Field(i)
|
||||
|
||||
assert.False(t, f.IsZero(), "Found zero-valued field %v",
|
||||
s.Type().Field(i).Name)
|
||||
|
||||
switch f := f.Interface().(type) {
|
||||
case int64, bytes.HexBytes, string:
|
||||
byteSlices = append(byteSlices, metadata.CdcEncode(f))
|
||||
case time.Time:
|
||||
bz, err := gogotypes.StdTimeMarshal(f)
|
||||
require.NoError(t, err)
|
||||
byteSlices = append(byteSlices, bz)
|
||||
case version.Consensus:
|
||||
pbc := tmversion.Consensus{
|
||||
Block: f.Block,
|
||||
App: f.App,
|
||||
}
|
||||
bz, err := pbc.Marshal()
|
||||
require.NoError(t, err)
|
||||
byteSlices = append(byteSlices, bz)
|
||||
case metadata.BlockID:
|
||||
pbbi := f.ToProto()
|
||||
bz, err := pbbi.Marshal()
|
||||
require.NoError(t, err)
|
||||
byteSlices = append(byteSlices, bz)
|
||||
default:
|
||||
t.Errorf("unknown type %T", f)
|
||||
}
|
||||
}
|
||||
assert.Equal(t,
|
||||
bytes.HexBytes(merkle.HashFromByteSlices(byteSlices)), tc.header.Hash())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxHeaderBytes(t *testing.T) {
|
||||
// Construct a UTF-8 string of MaxChainIDLen length using the supplementary
|
||||
// characters.
|
||||
// Each supplementary character takes 4 bytes.
|
||||
// http://www.i18nguy.com/unicode/supplementary-test.html
|
||||
maxChainID := ""
|
||||
for i := 0; i < metadata.MaxChainIDLen; i++ {
|
||||
maxChainID += "𠜎"
|
||||
}
|
||||
|
||||
// time is varint encoded so need to pick the max.
|
||||
// year int, month Month, day, hour, min, sec, nsec int, loc *Location
|
||||
timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC)
|
||||
|
||||
h := metadata.Header{
|
||||
Version: version.Consensus{Block: math.MaxInt64, App: math.MaxInt64},
|
||||
ChainID: maxChainID,
|
||||
Height: math.MaxInt64,
|
||||
Time: timestamp,
|
||||
LastBlockID: metadata.BlockID{make([]byte, tmhash.Size), metadata.PartSetHeader{math.MaxInt32, make([]byte, tmhash.Size)}},
|
||||
LastCommitHash: tmhash.Sum([]byte("last_commit_hash")),
|
||||
DataHash: tmhash.Sum([]byte("data_hash")),
|
||||
ValidatorsHash: tmhash.Sum([]byte("validators_hash")),
|
||||
NextValidatorsHash: tmhash.Sum([]byte("next_validators_hash")),
|
||||
ConsensusHash: tmhash.Sum([]byte("consensus_hash")),
|
||||
AppHash: tmhash.Sum([]byte("app_hash")),
|
||||
LastResultsHash: tmhash.Sum([]byte("last_results_hash")),
|
||||
EvidenceHash: tmhash.Sum([]byte("evidence_hash")),
|
||||
ProposerAddress: crypto.AddressHash([]byte("proposer_address")),
|
||||
}
|
||||
|
||||
bz, err := h.ToProto().Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, metadata.MaxHeaderBytes, int64(len(bz)))
|
||||
}
|
||||
|
||||
func randCommit(now time.Time) *metadata.Commit {
|
||||
lastID := test.MakeBlockID()
|
||||
h := int64(3)
|
||||
voteSet, _, vals := test.RandVoteSet(h-1, 1, tmproto.PrecommitType, 10, 1)
|
||||
commit, err := test.MakeCommit(lastID, h-1, 1, voteSet, vals, now)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return commit
|
||||
}
|
||||
|
||||
func hexBytesFromString(s string) bytes.HexBytes {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bytes.HexBytes(b)
|
||||
}
|
||||
|
||||
func TestHeader_ValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
header metadata.Header
|
||||
expectErr bool
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
"invalid version block",
|
||||
metadata.Header{Version: version.Consensus{Block: version.BlockProtocol + 1}},
|
||||
true, "block protocol is incorrect",
|
||||
},
|
||||
{
|
||||
"invalid chain ID length",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen+1)),
|
||||
},
|
||||
true, "chainID is too long",
|
||||
},
|
||||
{
|
||||
"invalid height (negative)",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: -1,
|
||||
},
|
||||
true, "negative Height",
|
||||
},
|
||||
{
|
||||
"invalid height (zero)",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 0,
|
||||
},
|
||||
true, "zero Height",
|
||||
},
|
||||
{
|
||||
"invalid block ID hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
},
|
||||
true, "wrong Hash",
|
||||
},
|
||||
{
|
||||
"invalid block ID parts header hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
},
|
||||
},
|
||||
true, "wrong PartSetHeader",
|
||||
},
|
||||
{
|
||||
"invalid last commit hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong LastCommitHash",
|
||||
},
|
||||
{
|
||||
"invalid data hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong DataHash",
|
||||
},
|
||||
{
|
||||
"invalid evidence hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong EvidenceHash",
|
||||
},
|
||||
{
|
||||
"invalid proposer address",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize+1),
|
||||
},
|
||||
true, "invalid ProposerAddress length",
|
||||
},
|
||||
{
|
||||
"invalid validator hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize),
|
||||
ValidatorsHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong ValidatorsHash",
|
||||
},
|
||||
{
|
||||
"invalid next validator hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize),
|
||||
ValidatorsHash: make([]byte, tmhash.Size),
|
||||
NextValidatorsHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong NextValidatorsHash",
|
||||
},
|
||||
{
|
||||
"invalid consensus hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize),
|
||||
ValidatorsHash: make([]byte, tmhash.Size),
|
||||
NextValidatorsHash: make([]byte, tmhash.Size),
|
||||
ConsensusHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong ConsensusHash",
|
||||
},
|
||||
{
|
||||
"invalid last results hash",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize),
|
||||
ValidatorsHash: make([]byte, tmhash.Size),
|
||||
NextValidatorsHash: make([]byte, tmhash.Size),
|
||||
ConsensusHash: make([]byte, tmhash.Size),
|
||||
LastResultsHash: make([]byte, tmhash.Size+1),
|
||||
},
|
||||
true, "wrong LastResultsHash",
|
||||
},
|
||||
{
|
||||
"valid header",
|
||||
metadata.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol},
|
||||
ChainID: string(make([]byte, metadata.MaxChainIDLen)),
|
||||
Height: 1,
|
||||
LastBlockID: metadata.BlockID{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Hash: make([]byte, tmhash.Size),
|
||||
},
|
||||
},
|
||||
LastCommitHash: make([]byte, tmhash.Size),
|
||||
DataHash: make([]byte, tmhash.Size),
|
||||
EvidenceHash: make([]byte, tmhash.Size),
|
||||
ProposerAddress: make([]byte, crypto.AddressSize),
|
||||
ValidatorsHash: make([]byte, tmhash.Size),
|
||||
NextValidatorsHash: make([]byte, tmhash.Size),
|
||||
ConsensusHash: make([]byte, tmhash.Size),
|
||||
LastResultsHash: make([]byte, tmhash.Size),
|
||||
},
|
||||
false, "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.header.ValidateBasic()
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.errString)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderProto(t *testing.T) {
|
||||
h1 := test.MakeRandomHeader()
|
||||
tc := []struct {
|
||||
msg string
|
||||
h1 *metadata.Header
|
||||
expPass bool
|
||||
}{
|
||||
{"success", h1, true},
|
||||
{"failure empty Header", &metadata.Header{}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tc {
|
||||
tt := tt
|
||||
t.Run(tt.msg, func(t *testing.T) {
|
||||
pb := tt.h1.ToProto()
|
||||
h, err := metadata.HeaderFromProto(pb)
|
||||
if tt.expPass {
|
||||
require.NoError(t, err, tt.msg)
|
||||
require.Equal(t, tt.h1, &h, tt.msg)
|
||||
} else {
|
||||
require.Error(t, err, tt.msg)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderHashVector(t *testing.T) {
|
||||
chainID := "test"
|
||||
h := metadata.Header{
|
||||
Version: version.Consensus{Block: 1, App: 1},
|
||||
ChainID: chainID,
|
||||
Height: 50,
|
||||
Time: time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC),
|
||||
LastBlockID: metadata.BlockID{},
|
||||
LastCommitHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
DataHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
ValidatorsHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
NextValidatorsHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
ConsensusHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
AppHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
|
||||
LastResultsHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
|
||||
EvidenceHash: []byte("f2564c78071e26643ae9b3e2a19fa0dc10d4d9e873aa0be808660123f11a1e78"),
|
||||
ProposerAddress: []byte("2915b7b15f979e48ebc61774bb1d86ba3136b7eb"),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
header metadata.Header
|
||||
expBytes string
|
||||
}{
|
||||
{header: h, expBytes: "87b6117ac7f827d656f178a3d6d30b24b205db2b6a3a053bae8baf4618570bfc"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
hash := tc.header.Hash()
|
||||
require.Equal(t, tc.expBytes, hex.EncodeToString(hash))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
// BlockID
|
||||
type BlockID struct {
|
||||
Hash tmbytes.HexBytes `json:"hash"`
|
||||
PartSetHeader PartSetHeader `json:"parts"`
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsZero returns true if this is the BlockID of a nil block.
|
||||
func (blockID BlockID) IsZero() 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()
|
||||
}
|
||||
|
||||
// ValidateHash returns an error if the hash is not empty, but its
|
||||
// size != tmhash.Size.
|
||||
func ValidateHash(h []byte) error {
|
||||
if len(h) > 0 && len(h) != tmhash.Size {
|
||||
return fmt.Errorf("expected size to be %d bytes, got %d bytes",
|
||||
tmhash.Size,
|
||||
len(h),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package metadata_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
test "github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/bytes"
|
||||
"github.com/tendermint/tendermint/pkg/metadata"
|
||||
)
|
||||
|
||||
func TestBlockIDEquals(t *testing.T) {
|
||||
var (
|
||||
blockID = metadata.BlockID{[]byte("hash"), metadata.PartSetHeader{2, []byte("part_set_hash")}}
|
||||
blockIDDuplicate = metadata.BlockID{[]byte("hash"), metadata.PartSetHeader{2, []byte("part_set_hash")}}
|
||||
blockIDDifferent = metadata.BlockID{[]byte("different_hash"), metadata.PartSetHeader{2, []byte("part_set_hash")}}
|
||||
blockIDEmpty = metadata.BlockID{}
|
||||
)
|
||||
|
||||
assert.True(t, blockID.Equals(blockIDDuplicate))
|
||||
assert.False(t, blockID.Equals(blockIDDifferent))
|
||||
assert.False(t, blockID.Equals(blockIDEmpty))
|
||||
assert.True(t, blockIDEmpty.Equals(blockIDEmpty))
|
||||
assert.False(t, blockIDEmpty.Equals(blockIDDifferent))
|
||||
}
|
||||
|
||||
func TestBlockIDValidateBasic(t *testing.T) {
|
||||
validBlockID := metadata.BlockID{
|
||||
Hash: bytes.HexBytes{},
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Total: 1,
|
||||
Hash: bytes.HexBytes{},
|
||||
},
|
||||
}
|
||||
|
||||
invalidBlockID := metadata.BlockID{
|
||||
Hash: []byte{0},
|
||||
PartSetHeader: metadata.PartSetHeader{
|
||||
Total: 1,
|
||||
Hash: []byte{0},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
blockIDHash bytes.HexBytes
|
||||
blockIDPartSetHeader metadata.PartSetHeader
|
||||
expectErr bool
|
||||
}{
|
||||
{"Valid BlockID", validBlockID.Hash, validBlockID.PartSetHeader, false},
|
||||
{"Invalid BlockID", invalidBlockID.Hash, validBlockID.PartSetHeader, true},
|
||||
{"Invalid BlockID", validBlockID.Hash, invalidBlockID.PartSetHeader, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
blockID := metadata.BlockID{
|
||||
Hash: tc.blockIDHash,
|
||||
PartSetHeader: tc.blockIDPartSetHeader,
|
||||
}
|
||||
assert.Equal(t, tc.expectErr, blockID.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockIDProtoBuf(t *testing.T) {
|
||||
blockID := test.MakeBlockIDWithHash([]byte("hash"))
|
||||
testCases := []struct {
|
||||
msg string
|
||||
bid1 *metadata.BlockID
|
||||
expPass bool
|
||||
}{
|
||||
{"success", &blockID, true},
|
||||
{"success empty", &metadata.BlockID{}, true},
|
||||
{"failure BlockID nil", nil, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
protoBlockID := tc.bid1.ToProto()
|
||||
|
||||
bi, err := metadata.BlockIDFromProto(&protoBlockID)
|
||||
if tc.expPass {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.bid1, bi, tc.msg)
|
||||
} else {
|
||||
require.NotEqual(t, tc.bid1, bi, tc.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/libs/bits"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
tmjson "github.com/tendermint/tendermint/libs/json"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPartSetUnexpectedIndex = errors.New("error part set unexpected index")
|
||||
ErrPartSetInvalidProof = errors.New("error part set invalid proof")
|
||||
)
|
||||
|
||||
const (
|
||||
// BlockPartSizeBytes is the size of one block part.
|
||||
BlockPartSizeBytes uint32 = 65536 // 64kB
|
||||
|
||||
// MaxBlockPartsCount is the maximum number of block parts.
|
||||
MaxBlockPartsCount = (MaxBlockSizeBytes / BlockPartSizeBytes) + 1
|
||||
)
|
||||
|
||||
type Part struct {
|
||||
Index uint32 `json:"index"`
|
||||
Bytes tmbytes.HexBytes `json:"bytes"`
|
||||
Proof merkle.Proof `json:"proof"`
|
||||
}
|
||||
|
||||
// ValidateBasic performs basic validation.
|
||||
func (part *Part) ValidateBasic() error {
|
||||
if len(part.Bytes) > int(BlockPartSizeBytes) {
|
||||
return fmt.Errorf("too big: %d bytes, max: %d", len(part.Bytes), BlockPartSizeBytes)
|
||||
}
|
||||
if err := part.Proof.ValidateBasic(); err != nil {
|
||||
return fmt.Errorf("wrong Proof: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of Part.
|
||||
//
|
||||
// See StringIndented.
|
||||
func (part *Part) String() string {
|
||||
return part.StringIndented("")
|
||||
}
|
||||
|
||||
// StringIndented returns an indented Part.
|
||||
//
|
||||
// See merkle.Proof#StringIndented
|
||||
func (part *Part) StringIndented(indent string) string {
|
||||
return fmt.Sprintf(`Part{#%v
|
||||
%s Bytes: %X...
|
||||
%s Proof: %v
|
||||
%s}`,
|
||||
part.Index,
|
||||
indent, tmbytes.Fingerprint(part.Bytes),
|
||||
indent, part.Proof.StringIndented(indent+" "),
|
||||
indent)
|
||||
}
|
||||
|
||||
func (part *Part) ToProto() (*tmproto.Part, error) {
|
||||
if part == nil {
|
||||
return nil, errors.New("nil part")
|
||||
}
|
||||
pb := new(tmproto.Part)
|
||||
proof := part.Proof.ToProto()
|
||||
|
||||
pb.Index = part.Index
|
||||
pb.Bytes = part.Bytes
|
||||
pb.Proof = *proof
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func PartFromProto(pb *tmproto.Part) (*Part, error) {
|
||||
if pb == nil {
|
||||
return nil, errors.New("nil part")
|
||||
}
|
||||
|
||||
part := new(Part)
|
||||
proof, err := merkle.ProofFromProto(&pb.Proof)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
part.Index = pb.Index
|
||||
part.Bytes = pb.Bytes
|
||||
part.Proof = *proof
|
||||
|
||||
return part, part.ValidateBasic()
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
|
||||
type PartSetHeader struct {
|
||||
Total uint32 `json:"total"`
|
||||
Hash tmbytes.HexBytes `json:"hash"`
|
||||
}
|
||||
|
||||
// String returns a string representation of PartSetHeader.
|
||||
//
|
||||
// 1. total number of parts
|
||||
// 2. first 6 bytes of the hash
|
||||
func (psh PartSetHeader) String() string {
|
||||
return fmt.Sprintf("%v:%X", psh.Total, tmbytes.Fingerprint(psh.Hash))
|
||||
}
|
||||
|
||||
func (psh PartSetHeader) IsZero() bool {
|
||||
return psh.Total == 0 && len(psh.Hash) == 0
|
||||
}
|
||||
|
||||
func (psh PartSetHeader) Equals(other PartSetHeader) bool {
|
||||
return psh.Total == other.Total && bytes.Equal(psh.Hash, other.Hash)
|
||||
}
|
||||
|
||||
// ValidateBasic performs basic validation.
|
||||
func (psh PartSetHeader) ValidateBasic() error {
|
||||
// Hash can be empty in case of POLBlockID.PartSetHeader in Proposal.
|
||||
if err := ValidateHash(psh.Hash); err != nil {
|
||||
return fmt.Errorf("wrong Hash: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToProto converts PartSetHeader to protobuf
|
||||
func (psh *PartSetHeader) ToProto() tmproto.PartSetHeader {
|
||||
if psh == nil {
|
||||
return tmproto.PartSetHeader{}
|
||||
}
|
||||
|
||||
return tmproto.PartSetHeader{
|
||||
Total: psh.Total,
|
||||
Hash: psh.Hash,
|
||||
}
|
||||
}
|
||||
|
||||
// FromProto sets a protobuf PartSetHeader to the given pointer
|
||||
func PartSetHeaderFromProto(ppsh *tmproto.PartSetHeader) (*PartSetHeader, error) {
|
||||
if ppsh == nil {
|
||||
return nil, errors.New("nil PartSetHeader")
|
||||
}
|
||||
psh := new(PartSetHeader)
|
||||
psh.Total = ppsh.Total
|
||||
psh.Hash = ppsh.Hash
|
||||
|
||||
return psh, psh.ValidateBasic()
|
||||
}
|
||||
|
||||
//-------------------------------------
|
||||
|
||||
type PartSet struct {
|
||||
total uint32
|
||||
hash []byte
|
||||
|
||||
mtx tmsync.Mutex
|
||||
parts []*Part
|
||||
partsBitArray *bits.BitArray
|
||||
count uint32
|
||||
// a count of the total size (in bytes). Used to ensure that the
|
||||
// part set doesn't exceed the maximum block bytes
|
||||
byteSize int64
|
||||
}
|
||||
|
||||
// Returns an immutable, full PartSet from the data bytes.
|
||||
// The data bytes are split into "partSize" chunks, and merkle tree computed.
|
||||
// CONTRACT: partSize is greater than zero.
|
||||
func NewPartSetFromData(data []byte, partSize uint32) *PartSet {
|
||||
// divide data into 4kb parts.
|
||||
total := (uint32(len(data)) + partSize - 1) / partSize
|
||||
parts := make([]*Part, total)
|
||||
partsBytes := make([][]byte, total)
|
||||
partsBitArray := bits.NewBitArray(int(total))
|
||||
for i := uint32(0); i < total; i++ {
|
||||
part := &Part{
|
||||
Index: i,
|
||||
Bytes: data[i*partSize : tmmath.MinInt(len(data), int((i+1)*partSize))],
|
||||
}
|
||||
parts[i] = part
|
||||
partsBytes[i] = part.Bytes
|
||||
partsBitArray.SetIndex(int(i), true)
|
||||
}
|
||||
// Compute merkle proofs
|
||||
root, proofs := merkle.ProofsFromByteSlices(partsBytes)
|
||||
for i := uint32(0); i < total; i++ {
|
||||
parts[i].Proof = *proofs[i]
|
||||
}
|
||||
return &PartSet{
|
||||
total: total,
|
||||
hash: root,
|
||||
parts: parts,
|
||||
partsBitArray: partsBitArray,
|
||||
count: total,
|
||||
byteSize: int64(len(data)),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an empty PartSet ready to be populated.
|
||||
func NewPartSetFromHeader(header PartSetHeader) *PartSet {
|
||||
return &PartSet{
|
||||
total: header.Total,
|
||||
hash: header.Hash,
|
||||
parts: make([]*Part, header.Total),
|
||||
partsBitArray: bits.NewBitArray(int(header.Total)),
|
||||
count: 0,
|
||||
byteSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PartSet) Header() PartSetHeader {
|
||||
if ps == nil {
|
||||
return PartSetHeader{}
|
||||
}
|
||||
return PartSetHeader{
|
||||
Total: ps.total,
|
||||
Hash: ps.hash,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PartSet) HasHeader(header PartSetHeader) bool {
|
||||
if ps == nil {
|
||||
return false
|
||||
}
|
||||
return ps.Header().Equals(header)
|
||||
}
|
||||
|
||||
func (ps *PartSet) BitArray() *bits.BitArray {
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
return ps.partsBitArray.Copy()
|
||||
}
|
||||
|
||||
func (ps *PartSet) Hash() []byte {
|
||||
if ps == nil {
|
||||
return merkle.HashFromByteSlices(nil)
|
||||
}
|
||||
return ps.hash
|
||||
}
|
||||
|
||||
func (ps *PartSet) HashesTo(hash []byte) bool {
|
||||
if ps == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(ps.hash, hash)
|
||||
}
|
||||
|
||||
func (ps *PartSet) Count() uint32 {
|
||||
if ps == nil {
|
||||
return 0
|
||||
}
|
||||
return ps.count
|
||||
}
|
||||
|
||||
func (ps *PartSet) ByteSize() int64 {
|
||||
if ps == nil {
|
||||
return 0
|
||||
}
|
||||
return ps.byteSize
|
||||
}
|
||||
|
||||
func (ps *PartSet) Total() uint32 {
|
||||
if ps == nil {
|
||||
return 0
|
||||
}
|
||||
return ps.total
|
||||
}
|
||||
|
||||
func (ps *PartSet) AddPart(part *Part) (bool, error) {
|
||||
if ps == nil {
|
||||
return false, nil
|
||||
}
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
|
||||
// Invalid part index
|
||||
if part.Index >= ps.total {
|
||||
return false, ErrPartSetUnexpectedIndex
|
||||
}
|
||||
|
||||
// If part already exists, return false.
|
||||
if ps.parts[part.Index] != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check hash proof
|
||||
if part.Proof.Verify(ps.Hash(), part.Bytes) != nil {
|
||||
return false, ErrPartSetInvalidProof
|
||||
}
|
||||
|
||||
// Add part
|
||||
ps.parts[part.Index] = part
|
||||
ps.partsBitArray.SetIndex(int(part.Index), true)
|
||||
ps.count++
|
||||
ps.byteSize += int64(len(part.Bytes))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ps *PartSet) GetPart(index int) *Part {
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
return ps.parts[index]
|
||||
}
|
||||
|
||||
func (ps *PartSet) IsComplete() bool {
|
||||
return ps.count == ps.total
|
||||
}
|
||||
|
||||
func (ps *PartSet) GetReader() io.Reader {
|
||||
if !ps.IsComplete() {
|
||||
panic("Cannot GetReader() on incomplete PartSet")
|
||||
}
|
||||
return NewPartSetReader(ps.parts)
|
||||
}
|
||||
|
||||
type PartSetReader struct {
|
||||
i int
|
||||
parts []*Part
|
||||
reader *bytes.Reader
|
||||
}
|
||||
|
||||
func NewPartSetReader(parts []*Part) *PartSetReader {
|
||||
return &PartSetReader{
|
||||
i: 0,
|
||||
parts: parts,
|
||||
reader: bytes.NewReader(parts[0].Bytes),
|
||||
}
|
||||
}
|
||||
|
||||
func (psr *PartSetReader) Read(p []byte) (n int, err error) {
|
||||
readerLen := psr.reader.Len()
|
||||
if readerLen >= len(p) {
|
||||
return psr.reader.Read(p)
|
||||
} else if readerLen > 0 {
|
||||
n1, err := psr.Read(p[:readerLen])
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := psr.Read(p[readerLen:])
|
||||
return n1 + n2, err
|
||||
}
|
||||
|
||||
psr.i++
|
||||
if psr.i >= len(psr.parts) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
psr.reader = bytes.NewReader(psr.parts[psr.i].Bytes)
|
||||
return psr.Read(p)
|
||||
}
|
||||
|
||||
// StringShort returns a short version of String.
|
||||
//
|
||||
// (Count of Total)
|
||||
func (ps *PartSet) StringShort() string {
|
||||
if ps == nil {
|
||||
return "nil-PartSet"
|
||||
}
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
return fmt.Sprintf("(%v of %v)", ps.Count(), ps.Total())
|
||||
}
|
||||
|
||||
func (ps *PartSet) MarshalJSON() ([]byte, error) {
|
||||
if ps == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
|
||||
return tmjson.Marshal(struct {
|
||||
CountTotal string `json:"count/total"`
|
||||
PartsBitArray *bits.BitArray `json:"parts_bit_array"`
|
||||
}{
|
||||
fmt.Sprintf("%d/%d", ps.Count(), ps.Total()),
|
||||
ps.partsBitArray,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
testPartSize = 65536 // 64KB ... 4096 // 4KB
|
||||
)
|
||||
|
||||
func TestBasicPartSet(t *testing.T) {
|
||||
// Construct random data of size partSize * 100
|
||||
nParts := 100
|
||||
data := tmrand.Bytes(testPartSize * nParts)
|
||||
partSet := NewPartSetFromData(data, testPartSize)
|
||||
|
||||
assert.NotEmpty(t, partSet.Hash())
|
||||
assert.EqualValues(t, nParts, partSet.Total())
|
||||
assert.Equal(t, nParts, partSet.BitArray().Size())
|
||||
assert.True(t, partSet.HashesTo(partSet.Hash()))
|
||||
assert.True(t, partSet.IsComplete())
|
||||
assert.EqualValues(t, nParts, partSet.Count())
|
||||
assert.EqualValues(t, testPartSize*nParts, partSet.ByteSize())
|
||||
|
||||
// Test adding parts to a new partSet.
|
||||
partSet2 := NewPartSetFromHeader(partSet.Header())
|
||||
|
||||
assert.True(t, partSet2.HasHeader(partSet.Header()))
|
||||
for i := 0; i < int(partSet.Total()); i++ {
|
||||
part := partSet.GetPart(i)
|
||||
// t.Logf("\n%v", part)
|
||||
added, err := partSet2.AddPart(part)
|
||||
if !added || err != nil {
|
||||
t.Errorf("failed to add part %v, error: %v", i, err)
|
||||
}
|
||||
}
|
||||
// adding part with invalid index
|
||||
added, err := partSet2.AddPart(&Part{Index: 10000})
|
||||
assert.False(t, added)
|
||||
assert.Error(t, err)
|
||||
// adding existing part
|
||||
added, err = partSet2.AddPart(partSet2.GetPart(0))
|
||||
assert.False(t, added)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, partSet.Hash(), partSet2.Hash())
|
||||
assert.EqualValues(t, nParts, partSet2.Total())
|
||||
assert.EqualValues(t, nParts*testPartSize, partSet.ByteSize())
|
||||
assert.True(t, partSet2.IsComplete())
|
||||
|
||||
// Reconstruct data, assert that they are equal.
|
||||
data2Reader := partSet2.GetReader()
|
||||
data2, err := ioutil.ReadAll(data2Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, data, data2)
|
||||
}
|
||||
|
||||
func TestWrongProof(t *testing.T) {
|
||||
// Construct random data of size partSize * 100
|
||||
data := tmrand.Bytes(testPartSize * 100)
|
||||
partSet := NewPartSetFromData(data, testPartSize)
|
||||
|
||||
// Test adding a part with wrong data.
|
||||
partSet2 := NewPartSetFromHeader(partSet.Header())
|
||||
|
||||
// Test adding a part with wrong trail.
|
||||
part := partSet.GetPart(0)
|
||||
part.Proof.Aunts[0][0] += byte(0x01)
|
||||
added, err := partSet2.AddPart(part)
|
||||
if added || err == nil {
|
||||
t.Errorf("expected to fail adding a part with bad trail.")
|
||||
}
|
||||
|
||||
// Test adding a part with wrong bytes.
|
||||
part = partSet.GetPart(1)
|
||||
part.Bytes[0] += byte(0x01)
|
||||
added, err = partSet2.AddPart(part)
|
||||
if added || err == nil {
|
||||
t.Errorf("expected to fail adding a part with bad bytes.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartSetHeaderValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
testName string
|
||||
malleatePartSetHeader func(*PartSetHeader)
|
||||
expectErr bool
|
||||
}{
|
||||
{"Good PartSet", func(psHeader *PartSetHeader) {}, false},
|
||||
{"Invalid Hash", func(psHeader *PartSetHeader) { psHeader.Hash = make([]byte, 1) }, true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
data := tmrand.Bytes(testPartSize * 100)
|
||||
ps := NewPartSetFromData(data, testPartSize)
|
||||
psHeader := ps.Header()
|
||||
tc.malleatePartSetHeader(&psHeader)
|
||||
assert.Equal(t, tc.expectErr, psHeader.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartValidateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
testName string
|
||||
malleatePart func(*Part)
|
||||
expectErr bool
|
||||
}{
|
||||
{"Good Part", func(pt *Part) {}, false},
|
||||
{"Too big part", func(pt *Part) { pt.Bytes = make([]byte, BlockPartSizeBytes+1) }, true},
|
||||
{"Too big proof", func(pt *Part) {
|
||||
pt.Proof = merkle.Proof{
|
||||
Total: 1,
|
||||
Index: 1,
|
||||
LeafHash: make([]byte, 1024*1024),
|
||||
}
|
||||
}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
data := tmrand.Bytes(testPartSize * 100)
|
||||
ps := NewPartSetFromData(data, testPartSize)
|
||||
part := ps.GetPart(0)
|
||||
tc.malleatePart(part)
|
||||
assert.Equal(t, tc.expectErr, part.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParSetHeaderProtoBuf(t *testing.T) {
|
||||
testCases := []struct {
|
||||
msg string
|
||||
ps1 *PartSetHeader
|
||||
expPass bool
|
||||
}{
|
||||
{"success empty", &PartSetHeader{}, true},
|
||||
{"success",
|
||||
&PartSetHeader{Total: 1, Hash: []byte("hash")}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
protoBlockID := tc.ps1.ToProto()
|
||||
|
||||
psh, err := PartSetHeaderFromProto(&protoBlockID)
|
||||
if tc.expPass {
|
||||
require.Equal(t, tc.ps1, psh, tc.msg)
|
||||
} else {
|
||||
require.Error(t, err, tc.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartProtoBuf(t *testing.T) {
|
||||
|
||||
proof := merkle.Proof{
|
||||
Total: 1,
|
||||
Index: 1,
|
||||
LeafHash: tmrand.Bytes(32),
|
||||
}
|
||||
testCases := []struct {
|
||||
msg string
|
||||
ps1 *Part
|
||||
expPass bool
|
||||
}{
|
||||
{"failure empty", &Part{}, false},
|
||||
{"failure nil", nil, false},
|
||||
{"success",
|
||||
&Part{Index: 1, Bytes: tmrand.Bytes(32), Proof: proof}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
proto, err := tc.ps1.ToProto()
|
||||
if tc.expPass {
|
||||
require.NoError(t, err, tc.msg)
|
||||
}
|
||||
|
||||
p, err := PartFromProto(proto)
|
||||
if tc.expPass {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.ps1, p, tc.msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
)
|
||||
|
||||
// Go lacks a simple and safe way to see if something is a typed nil.
|
||||
// See:
|
||||
// - https://dave.cheney.net/2017/08/09/typed-nils-in-go-2
|
||||
// - https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I/discussion
|
||||
// - https://github.com/golang/go/issues/21538
|
||||
func isTypedNil(o interface{}) bool {
|
||||
rv := reflect.ValueOf(o)
|
||||
switch rv.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return rv.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if it has zero length.
|
||||
func isEmpty(o interface{}) bool {
|
||||
rv := reflect.ValueOf(o)
|
||||
switch rv.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
|
||||
return rv.Len() == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CdcEncode returns nil if the input is nil, otherwise returns
|
||||
// proto.Marshal(<type>Value{Value: item})
|
||||
func CdcEncode(item interface{}) []byte {
|
||||
if item != nil && !isTypedNil(item) && !isEmpty(item) {
|
||||
switch item := item.(type) {
|
||||
case string:
|
||||
i := gogotypes.StringValue{
|
||||
Value: item,
|
||||
}
|
||||
bz, err := i.Marshal()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return bz
|
||||
case int64:
|
||||
i := gogotypes.Int64Value{
|
||||
Value: item,
|
||||
}
|
||||
bz, err := i.Marshal()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return bz
|
||||
case tmbytes.HexBytes:
|
||||
i := gogotypes.BytesValue{
|
||||
Value: item,
|
||||
}
|
||||
bz, err := i.Marshal()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return bz
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user