diff --git a/consensus/common_test.go b/consensus/common_test.go index 23e56e67e..377f1929f 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -74,6 +74,7 @@ func (vs *validatorStub) signVote(voteType byte, hash []byte, header types.PartS ValidatorAddress: vs.PrivValidator.GetAddress(), Height: vs.Height, Round: vs.Round, + Timestamp: time.Now().UTC(), Type: voteType, BlockID: types.BlockID{hash, header}, } diff --git a/consensus/state.go b/consensus/state.go index eedc30bc0..68133f53a 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1466,6 +1466,7 @@ func (cs *ConsensusState) signVote(type_ byte, hash []byte, header types.PartSet ValidatorIndex: valIndex, Height: cs.Height, Round: cs.Round, + Timestamp: time.Now().UTC(), Type: type_, BlockID: types.BlockID{hash, header}, } diff --git a/consensus/types/height_vote_set_test.go b/consensus/types/height_vote_set_test.go index e09d1419d..306592aa2 100644 --- a/consensus/types/height_vote_set_test.go +++ b/consensus/types/height_vote_set_test.go @@ -2,6 +2,7 @@ package types import ( "testing" + "time" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/types" @@ -54,6 +55,7 @@ func makeVoteHR(t *testing.T, height int64, round int, privVals []*types.PrivVal ValidatorIndex: valIndex, Height: height, Round: round, + Timestamp: time.Now().UTC(), Type: types.VoteTypePrecommit, BlockID: types.BlockID{[]byte("fakehash"), types.PartSetHeader{}}, } diff --git a/lite/helpers.go b/lite/helpers.go index 9319c4590..5702203ba 100644 --- a/lite/helpers.go +++ b/lite/helpers.go @@ -97,6 +97,7 @@ func makeVote(header *types.Header, vals *types.ValidatorSet, key crypto.PrivKey ValidatorIndex: idx, Height: header.Height, Round: 1, + Timestamp: time.Now().UTC(), Type: types.VoteTypePrecommit, BlockID: types.BlockID{Hash: header.Hash()}, } diff --git a/types/canonical_json.go b/types/canonical_json.go index a2e91164f..41c67c24b 100644 --- a/types/canonical_json.go +++ b/types/canonical_json.go @@ -1,11 +1,17 @@ package types import ( + "time" + + wire "github.com/tendermint/go-wire" "github.com/tendermint/go-wire/data" ) // canonical json is go-wire's json for structs with fields in alphabetical order +// timeFormat is used for generating the sigs +const timeFormat = wire.RFC3339Millis + type CanonicalJSONBlockID struct { Hash data.Bytes `json:"hash,omitempty"` PartsHeader CanonicalJSONPartSetHeader `json:"parts,omitempty"` @@ -22,13 +28,15 @@ type CanonicalJSONProposal struct { POLBlockID CanonicalJSONBlockID `json:"pol_block_id"` POLRound int `json:"pol_round"` Round int `json:"round"` + Timestamp string `json:"timestamp"` } type CanonicalJSONVote struct { - BlockID CanonicalJSONBlockID `json:"block_id"` - Height int64 `json:"height"` - Round int `json:"round"` - Type byte `json:"type"` + BlockID CanonicalJSONBlockID `json:"block_id"` + Height int64 `json:"height"` + Round int `json:"round"` + Timestamp string `json:"timestamp"` + Type byte `json:"type"` } type CanonicalJSONHeartbeat struct { @@ -78,6 +86,7 @@ func CanonicalProposal(proposal *Proposal) CanonicalJSONProposal { return CanonicalJSONProposal{ BlockPartsHeader: CanonicalPartSetHeader(proposal.BlockPartsHeader), Height: proposal.Height, + Timestamp: CanonicalTime(proposal.Timestamp), POLBlockID: CanonicalBlockID(proposal.POLBlockID), POLRound: proposal.POLRound, Round: proposal.Round, @@ -86,10 +95,11 @@ func CanonicalProposal(proposal *Proposal) CanonicalJSONProposal { func CanonicalVote(vote *Vote) CanonicalJSONVote { return CanonicalJSONVote{ - CanonicalBlockID(vote.BlockID), - vote.Height, - vote.Round, - vote.Type, + BlockID: CanonicalBlockID(vote.BlockID), + Height: vote.Height, + Round: vote.Round, + Timestamp: CanonicalTime(vote.Timestamp), + Type: vote.Type, } } @@ -102,3 +112,10 @@ func CanonicalHeartbeat(heartbeat *Heartbeat) CanonicalJSONHeartbeat { heartbeat.ValidatorIndex, } } + +func CanonicalTime(t time.Time) string { + // note that sending time over go-wire resets it to + // local time, we need to force UTC here, so the + // signatures match + return t.UTC().Format(timeFormat) +} diff --git a/types/priv_validator_test.go b/types/priv_validator_test.go index 3b13ed909..4e55fd49b 100644 --- a/types/priv_validator_test.go +++ b/types/priv_validator_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -165,6 +166,7 @@ func newVote(addr data.Bytes, idx int, height int64, round int, typ byte, blockI Height: height, Round: round, Type: typ, + Timestamp: time.Now().UTC(), BlockID: blockID, } } diff --git a/types/proposal.go b/types/proposal.go index 93e788961..98600681a 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "time" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" @@ -22,6 +23,7 @@ var ( type Proposal struct { Height int64 `json:"height"` Round int `json:"round"` + Timestamp time.Time `json:"timestamp"` BlockPartsHeader PartSetHeader `json:"block_parts_header"` POLRound int `json:"pol_round"` // -1 if null. POLBlockID BlockID `json:"pol_block_id"` // zero if null. @@ -34,6 +36,7 @@ func NewProposal(height int64, round int, blockPartsHeader PartSetHeader, polRou return &Proposal{ Height: height, Round: round, + Timestamp: time.Now().UTC(), BlockPartsHeader: blockPartsHeader, POLRound: polRound, POLBlockID: polBlockID, @@ -42,8 +45,9 @@ func NewProposal(height int64, round int, blockPartsHeader PartSetHeader, polRou // String returns a string representation of the Proposal. func (p *Proposal) String() string { - return fmt.Sprintf("Proposal{%v/%v %v (%v,%v) %v}", p.Height, p.Round, - p.BlockPartsHeader, p.POLRound, p.POLBlockID, p.Signature) + return fmt.Sprintf("Proposal{%v/%v %v (%v,%v) %v @ %s}", + p.Height, p.Round, p.BlockPartsHeader, p.POLRound, + p.POLBlockID, p.Signature, CanonicalTime(p.Timestamp)) } // WriteSignBytes writes the Proposal bytes for signing diff --git a/types/proposal_test.go b/types/proposal_test.go index 352ba8de1..0d2af71e2 100644 --- a/types/proposal_test.go +++ b/types/proposal_test.go @@ -2,25 +2,75 @@ package types import ( "testing" + "time" + + "github.com/stretchr/testify/require" + + wire "github.com/tendermint/go-wire" ) -var testProposal = &Proposal{ - Height: 12345, - Round: 23456, - BlockPartsHeader: PartSetHeader{111, []byte("blockparts")}, - POLRound: -1, +var testProposal *Proposal + +func init() { + var stamp, err = time.Parse(timeFormat, "2018-02-11T07:09:22.765Z") + if err != nil { + panic(err) + } + testProposal = &Proposal{ + Height: 12345, + Round: 23456, + BlockPartsHeader: PartSetHeader{111, []byte("blockparts")}, + POLRound: -1, + Timestamp: stamp, + } } func TestProposalSignable(t *testing.T) { signBytes := SignBytes("test_chain_id", testProposal) signStr := string(signBytes) - expected := `{"chain_id":"test_chain_id","proposal":{"block_parts_header":{"hash":"626C6F636B7061727473","total":111},"height":12345,"pol_block_id":{},"pol_round":-1,"round":23456}}` + expected := `{"chain_id":"test_chain_id","proposal":{"block_parts_header":{"hash":"626C6F636B7061727473","total":111},"height":12345,"pol_block_id":{},"pol_round":-1,"round":23456,"timestamp":"2018-02-11T07:09:22.765Z"}}` if signStr != expected { t.Errorf("Got unexpected sign string for Proposal. Expected:\n%v\nGot:\n%v", expected, signStr) } } +func TestProposalString(t *testing.T) { + str := testProposal.String() + expected := `Proposal{12345/23456 111:626C6F636B70 (-1,:0: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) { + privVal := GenPrivValidatorFS("") + pubKey := privVal.GetPubKey() + + prop := NewProposal(4, 2, PartSetHeader{777, []byte("proper")}, 2, BlockID{}) + signBytes := SignBytes("test_chain_id", prop) + + // sign it + signature, err := privVal.Signer.Sign(signBytes) + require.NoError(t, err) + + // verify the same proposal + valid := pubKey.VerifyBytes(SignBytes("test_chain_id", prop), signature) + require.True(t, valid) + + // serialize, deserialize and verify again.... + newProp := new(Proposal) + bs := wire.BinaryBytes(prop) + err = wire.ReadBinaryBytes(bs, &newProp) + require.NoError(t, err) + + // verify the transmitted proposal + newSignBytes := SignBytes("test_chain_id", newProp) + require.Equal(t, string(signBytes), string(newSignBytes)) + valid = pubKey.VerifyBytes(newSignBytes, signature) + require.True(t, valid) +} + func BenchmarkProposalWriteSignBytes(b *testing.B) { for i := 0; i < b.N; i++ { SignBytes("test_chain_id", testProposal) diff --git a/types/vote.go b/types/vote.go index bb8679f46..aa993e33f 100644 --- a/types/vote.go +++ b/types/vote.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "time" "github.com/tendermint/go-crypto" "github.com/tendermint/go-wire" @@ -53,6 +54,7 @@ type Vote struct { ValidatorIndex int `json:"validator_index"` Height int64 `json:"height"` Round int `json:"round"` + Timestamp time.Time `json:"timestamp"` Type byte `json:"type"` BlockID BlockID `json:"block_id"` // zero if vote is nil. Signature crypto.Signature `json:"signature"` @@ -84,8 +86,9 @@ func (vote *Vote) String() string { cmn.PanicSanity("Unknown vote type") } - return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %v}", + return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %v @ %s}", vote.ValidatorIndex, cmn.Fingerprint(vote.ValidatorAddress), vote.Height, vote.Round, vote.Type, typeString, - cmn.Fingerprint(vote.BlockID.Hash), vote.Signature) + cmn.Fingerprint(vote.BlockID.Hash), vote.Signature, + CanonicalTime(vote.Timestamp)) } diff --git a/types/vote_set_test.go b/types/vote_set_test.go index b093c44fd..d125c5502 100644 --- a/types/vote_set_test.go +++ b/types/vote_set_test.go @@ -3,6 +3,7 @@ package types import ( "bytes" "testing" + "time" crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" @@ -92,6 +93,7 @@ func TestAddVote(t *testing.T) { Height: height, Round: round, Type: VoteTypePrevote, + Timestamp: time.Now().UTC(), BlockID: BlockID{nil, PartSetHeader{}}, } _, err := signAddVote(val0, vote, voteSet) @@ -121,6 +123,7 @@ func Test2_3Majority(t *testing.T) { Height: height, Round: round, Type: VoteTypePrevote, + Timestamp: time.Now().UTC(), BlockID: BlockID{nil, PartSetHeader{}}, } // 6 out of 10 voted for nil. @@ -176,6 +179,7 @@ func Test2_3MajorityRedux(t *testing.T) { ValidatorIndex: -1, // NOTE: must fill in Height: height, Round: round, + Timestamp: time.Now().UTC(), Type: VoteTypePrevote, BlockID: BlockID{blockHash, blockPartsHeader}, } @@ -270,6 +274,7 @@ func TestBadVotes(t *testing.T) { ValidatorIndex: -1, Height: height, Round: round, + Timestamp: time.Now().UTC(), Type: VoteTypePrevote, BlockID: BlockID{nil, PartSetHeader{}}, } @@ -331,6 +336,7 @@ func TestConflicts(t *testing.T) { ValidatorIndex: -1, Height: height, Round: round, + Timestamp: time.Now().UTC(), Type: VoteTypePrevote, BlockID: BlockID{nil, PartSetHeader{}}, } @@ -459,6 +465,7 @@ func TestMakeCommit(t *testing.T) { ValidatorIndex: -1, Height: height, Round: round, + Timestamp: time.Now().UTC(), Type: VoteTypePrecommit, BlockID: BlockID{blockHash, blockPartsHeader}, } diff --git a/types/vote_test.go b/types/vote_test.go index 353acfaf5..032cc9472 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -2,14 +2,25 @@ package types import ( "testing" + "time" + + "github.com/stretchr/testify/require" + + wire "github.com/tendermint/go-wire" ) -func TestVoteSignable(t *testing.T) { - vote := &Vote{ +func exampleVote() *Vote { + var stamp, err = time.Parse(timeFormat, "2017-12-25T03:00:01.234Z") + if err != nil { + panic(err) + } + + return &Vote{ ValidatorAddress: []byte("addr"), ValidatorIndex: 56789, Height: 12345, - Round: 23456, + Round: 2, + Timestamp: stamp, Type: byte(2), BlockID: BlockID{ Hash: []byte("hash"), @@ -19,12 +30,52 @@ func TestVoteSignable(t *testing.T) { }, }, } +} + +func TestVoteSignable(t *testing.T) { + vote := exampleVote() signBytes := SignBytes("test_chain_id", vote) signStr := string(signBytes) - expected := `{"chain_id":"test_chain_id","vote":{"block_id":{"hash":"68617368","parts":{"hash":"70617274735F68617368","total":1000000}},"height":12345,"round":23456,"type":2}}` + expected := `{"chain_id":"test_chain_id","vote":{"block_id":{"hash":"68617368","parts":{"hash":"70617274735F68617368","total":1000000}},"height":12345,"round":2,"timestamp":"2017-12-25T03:00:01.234Z","type":2}}` if signStr != expected { // NOTE: when this fails, you probably want to fix up consensus/replay_test too t.Errorf("Got unexpected sign string for Vote. Expected:\n%v\nGot:\n%v", expected, signStr) } } + +func TestVoteString(t *testing.T) { + str := exampleVote().String() + expected := `Vote{56789:616464720000 12345/02/2(Precommit) 686173680000 {} @ 2017-12-25T03:00:01.234Z}` + if str != expected { + t.Errorf("Got unexpected string for Proposal. Expected:\n%v\nGot:\n%v", expected, str) + } +} + +func TestVoteVerifySignature(t *testing.T) { + privVal := GenPrivValidatorFS("") + pubKey := privVal.GetPubKey() + + vote := exampleVote() + signBytes := SignBytes("test_chain_id", vote) + + // sign it + signature, err := privVal.Signer.Sign(signBytes) + require.NoError(t, err) + + // verify the same vote + valid := pubKey.VerifyBytes(SignBytes("test_chain_id", vote), signature) + require.True(t, valid) + + // serialize, deserialize and verify again.... + precommit := new(Vote) + bs := wire.BinaryBytes(vote) + err = wire.ReadBinaryBytes(bs, &precommit) + require.NoError(t, err) + + // verify the transmitted vote + newSignBytes := SignBytes("test_chain_id", precommit) + require.Equal(t, string(signBytes), string(newSignBytes)) + valid = pubKey.VerifyBytes(newSignBytes, signature) + require.True(t, valid) +}