package types
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/tendermint/tendermint/crypto/tmhash"
|
|
"github.com/tendermint/tendermint/internal/libs/protoio"
|
|
tmrand "github.com/tendermint/tendermint/libs/rand"
|
|
tmtime "github.com/tendermint/tendermint/libs/time"
|
|
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
|
)
|
|
|
|
func getTestProposal(t testing.TB) *Proposal {
|
|
t.Helper()
|
|
|
|
stamp, err := time.Parse(TimeFormat, "2018-02-11T07:09:22.765Z")
|
|
require.NoError(t, err)
|
|
|
|
return &Proposal{
|
|
Height: 12345,
|
|
Round: 23456,
|
|
BlockID: BlockID{Hash: []byte("--June_15_2020_amino_was_removed"),
|
|
PartSetHeader: PartSetHeader{Total: 111, Hash: []byte("--June_15_2020_amino_was_removed")}},
|
|
POLRound: -1,
|
|
Timestamp: stamp,
|
|
}
|
|
}
|
|
|
|
func TestProposalSignable(t *testing.T) {
|
|
chainID := "test_chain_id"
|
|
signBytes := ProposalSignBytes(chainID, getTestProposal(t).ToProto())
|
|
pb := CanonicalizeProposal(chainID, getTestProposal(t).ToProto())
|
|
|
|
expected, err := protoio.MarshalDelimited(&pb)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, signBytes, "Got unexpected sign bytes for Proposal")
|
|
}
|
|
|
|
func TestProposalString(t *testing.T) {
|
|
str := getTestProposal(t).String()
|
|
expected := `Proposal{12345/23456 (2D2D4A756E655F31355F323032305F616D696E6F5F7761735F72656D6F766564:111:2D2D4A756E65, -1) 000000000000 @ 2018-02-11T07:09:22.765Z}`
|
|
if str != expected {
|
|
t.Errorf("got unexpected string for Proposal. Expected:\n%v\nGot:\n%v", expected, str)
|
|
}
|
|
}
|
|
|
|
func TestProposalVerifySignature(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
privVal := NewMockPV()
|
|
pubKey, err := privVal.GetPubKey(ctx)
|
|
require.NoError(t, err)
|
|
|
|
prop := NewProposal(
|
|
4, 2, 2,
|
|
BlockID{tmrand.Bytes(tmhash.Size), PartSetHeader{777, tmrand.Bytes(tmhash.Size)}}, tmtime.Now())
|
|
p := prop.ToProto()
|
|
signBytes := ProposalSignBytes("test_chain_id", p)
|
|
|
|
// sign it
|
|
err = privVal.SignProposal(ctx, "test_chain_id", p)
|
|
require.NoError(t, err)
|
|
prop.Signature = p.Signature
|
|
|
|
// verify the same proposal
|
|
valid := pubKey.VerifySignature(signBytes, prop.Signature)
|
|
require.True(t, valid)
|
|
|
|
// serialize, deserialize and verify again....
|
|
newProp := new(tmproto.Proposal)
|
|
pb := prop.ToProto()
|
|
|
|
bs, err := proto.Marshal(pb)
|
|
require.NoError(t, err)
|
|
|
|
err = proto.Unmarshal(bs, newProp)
|
|
require.NoError(t, err)
|
|
|
|
np, err := ProposalFromProto(newProp)
|
|
require.NoError(t, err)
|
|
|
|
// verify the transmitted proposal
|
|
newSignBytes := ProposalSignBytes("test_chain_id", pb)
|
|
require.Equal(t, string(signBytes), string(newSignBytes))
|
|
valid = pubKey.VerifySignature(newSignBytes, np.Signature)
|
|
require.True(t, valid)
|
|
}
|
|
|
|
func BenchmarkProposalWriteSignBytes(b *testing.B) {
|
|
pbp := getTestProposal(b).ToProto()
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
ProposalSignBytes("test_chain_id", pbp)
|
|
}
|
|
}
|
|
|
|
func BenchmarkProposalSign(b *testing.B) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
privVal := NewMockPV()
|
|
|
|
pbp := getTestProposal(b).ToProto()
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
err := privVal.SignProposal(ctx, "test_chain_id", pbp)
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkProposalVerifySignature(b *testing.B) {
|
|
testProposal := getTestProposal(b)
|
|
pbp := testProposal.ToProto()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
privVal := NewMockPV()
|
|
err := privVal.SignProposal(ctx, "test_chain_id", pbp)
|
|
require.NoError(b, err)
|
|
pubKey, err := privVal.GetPubKey(ctx)
|
|
require.NoError(b, err)
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
pubKey.VerifySignature(ProposalSignBytes("test_chain_id", pbp), testProposal.Signature)
|
|
}
|
|
}
|
|
|
|
func TestProposalValidateBasic(t *testing.T) {
|
|
|
|
privVal := NewMockPV()
|
|
testCases := []struct {
|
|
testName string
|
|
malleateProposal func(*Proposal)
|
|
expectErr bool
|
|
}{
|
|
{"Good Proposal", func(p *Proposal) {}, false},
|
|
{"Invalid Type", func(p *Proposal) { p.Type = tmproto.PrecommitType }, true},
|
|
{"Invalid Height", func(p *Proposal) { p.Height = -1 }, true},
|
|
{"Invalid Round", func(p *Proposal) { p.Round = -1 }, true},
|
|
{"Invalid POLRound", func(p *Proposal) { p.POLRound = -2 }, true},
|
|
{"Invalid BlockId", func(p *Proposal) {
|
|
p.BlockID = BlockID{[]byte{1, 2, 3}, PartSetHeader{111, []byte("blockparts")}}
|
|
}, true},
|
|
{"Invalid Signature", func(p *Proposal) {
|
|
p.Signature = make([]byte, 0)
|
|
}, true},
|
|
{"Too big Signature", func(p *Proposal) {
|
|
p.Signature = make([]byte, MaxSignatureSize+1)
|
|
}, true},
|
|
}
|
|
blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt32, tmhash.Sum([]byte("partshash")))
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
prop := NewProposal(
|
|
4, 2, 2,
|
|
blockID, tmtime.Now())
|
|
p := prop.ToProto()
|
|
err := privVal.SignProposal(ctx, "test_chain_id", p)
|
|
prop.Signature = p.Signature
|
|
require.NoError(t, err)
|
|
tc.malleateProposal(prop)
|
|
assert.Equal(t, tc.expectErr, prop.ValidateBasic() != nil, "Validate Basic had an unexpected result")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProposalProtoBuf(t *testing.T) {
|
|
proposal := NewProposal(1, 2, 3, makeBlockID([]byte("hash"), 2, []byte("part_set_hash")), tmtime.Now())
|
|
proposal.Signature = []byte("sig")
|
|
proposal2 := NewProposal(1, 2, 3, BlockID{}, tmtime.Now())
|
|
|
|
testCases := []struct {
|
|
msg string
|
|
p1 *Proposal
|
|
expPass bool
|
|
}{
|
|
{"success", proposal, true},
|
|
{"success", proposal2, false}, // blcokID cannot be empty
|
|
{"empty proposal failure validatebasic", &Proposal{}, false},
|
|
{"nil proposal", nil, false},
|
|
}
|
|
for _, tc := range testCases {
|
|
protoProposal := tc.p1.ToProto()
|
|
|
|
p, err := ProposalFromProto(protoProposal)
|
|
if tc.expPass {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.p1, p, tc.msg)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsTimely(t *testing.T) {
|
|
genesisTime, err := time.Parse(time.RFC3339, "2019-03-13T23:00:00Z")
|
|
require.NoError(t, err)
|
|
testCases := []struct {
|
|
name string
|
|
proposalTime time.Time
|
|
recvTime time.Time
|
|
precision time.Duration
|
|
msgDelay time.Duration
|
|
expectTimely bool
|
|
round int32
|
|
}{
|
|
// proposalTime - precision <= localTime <= proposalTime + msgDelay + precision
|
|
{
|
|
// Checking that the following inequality evaluates to true:
|
|
// 0 - 2 <= 1 <= 0 + 1 + 2
|
|
name: "basic timely",
|
|
proposalTime: genesisTime,
|
|
recvTime: genesisTime.Add(1 * time.Nanosecond),
|
|
precision: time.Nanosecond * 2,
|
|
msgDelay: time.Nanosecond,
|
|
expectTimely: true,
|
|
},
|
|
{
|
|
// Checking that the following inequality evaluates to false:
|
|
// 0 - 2 <= 4 <= 0 + 1 + 2
|
|
name: "local time too large",
|
|
proposalTime: genesisTime,
|
|
recvTime: genesisTime.Add(4 * time.Nanosecond),
|
|
precision: time.Nanosecond * 2,
|
|
msgDelay: time.Nanosecond,
|
|
expectTimely: false,
|
|
},
|
|
{
|
|
// Checking that the following inequality evaluates to false:
|
|
// 4 - 2 <= 0 <= 4 + 2 + 1
|
|
name: "proposal time too large",
|
|
proposalTime: genesisTime.Add(4 * time.Nanosecond),
|
|
recvTime: genesisTime,
|
|
precision: time.Nanosecond * 2,
|
|
msgDelay: time.Nanosecond,
|
|
expectTimely: false,
|
|
},
|
|
{
|
|
// Checking that the following inequality evaluates to true:
|
|
// 0 - (2 * 2) <= 4 <= 0 + (1 * 2) + 2
|
|
name: "message delay adapts after 10 rounds",
|
|
proposalTime: genesisTime,
|
|
recvTime: genesisTime.Add(4 * time.Nanosecond),
|
|
precision: time.Nanosecond * 2,
|
|
msgDelay: time.Nanosecond,
|
|
expectTimely: true,
|
|
round: 10,
|
|
},
|
|
{
|
|
// check that values that overflow time.Duration still correctly register
|
|
// as timely when round relaxation applied.
|
|
name: "message delay fixed to not overflow time.Duration",
|
|
proposalTime: genesisTime,
|
|
recvTime: genesisTime.Add(4 * time.Nanosecond),
|
|
precision: time.Nanosecond * 2,
|
|
msgDelay: time.Nanosecond,
|
|
expectTimely: true,
|
|
round: 5000,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
p := Proposal{
|
|
Timestamp: testCase.proposalTime,
|
|
}
|
|
|
|
sp := SynchronyParams{
|
|
Precision: testCase.precision,
|
|
MessageDelay: testCase.msgDelay,
|
|
}
|
|
|
|
ti := p.IsTimely(testCase.recvTime, sp, testCase.round)
|
|
assert.Equal(t, testCase.expectTimely, ti)
|
|
})
|
|
}
|
|
}
|