From 3f9d9cb36178d84d7136074e7e1696acdccbc958 Mon Sep 17 00:00:00 2001 From: Silas Davis <silas@monax.io> Date: Mon, 29 Jan 2018 21:20:20 +0000 Subject: [PATCH] Update PrivValidator to pre-released verifier types Signed-off-by: Silas Davis <silas@monax.io> --- .../validator/priv_validator_memory.go | 15 +- consensus/tendermint/validator/sign_info.go | 240 ++++++++++++++++++ consensus/tendermint/validator/verifier.go | 143 ----------- genesis/genesis.go | 4 - 4 files changed, 249 insertions(+), 153 deletions(-) create mode 100644 consensus/tendermint/validator/sign_info.go delete mode 100644 consensus/tendermint/validator/verifier.go diff --git a/consensus/tendermint/validator/priv_validator_memory.go b/consensus/tendermint/validator/priv_validator_memory.go index 90a51fa9..9e450bb7 100644 --- a/consensus/tendermint/validator/priv_validator_memory.go +++ b/consensus/tendermint/validator/priv_validator_memory.go @@ -9,7 +9,7 @@ import ( type privValidatorMemory struct { acm.Addressable - acm.Signer + tm_types.Signer lastSignedInfo *LastSignedInfo } @@ -20,8 +20,8 @@ var _ tm_types.PrivValidator = &privValidatorMemory{} func NewPrivValidatorMemory(addressable acm.Addressable, signer acm.Signer) *privValidatorMemory { return &privValidatorMemory{ Addressable: addressable, - Signer: signer, - lastSignedInfo: new(LastSignedInfo), + Signer: asTendermintSigner(signer), + lastSignedInfo: NewLastSignedInfo(), } } @@ -33,16 +33,19 @@ func (pvm *privValidatorMemory) GetPubKey() crypto.PubKey { return pvm.PublicKey().PubKey } +// TODO: consider persistence to disk/database to avoid double signing after a crash func (pvm *privValidatorMemory) SignVote(chainID string, vote *tm_types.Vote) error { - return pvm.lastSignedInfo.SignVote(asTendermintSigner(pvm.Signer), chainID, vote) + return pvm.lastSignedInfo.SignVote(pvm.Signer, chainID, vote) } func (pvm *privValidatorMemory) SignProposal(chainID string, proposal *tm_types.Proposal) error { - return pvm.lastSignedInfo.SignProposal(asTendermintSigner(pvm.Signer), chainID, proposal) + return pvm.lastSignedInfo.SignProposal(pvm.Signer, chainID, proposal) } func (pvm *privValidatorMemory) SignHeartbeat(chainID string, heartbeat *tm_types.Heartbeat) error { - return pvm.lastSignedInfo.SignHeartbeat(asTendermintSigner(pvm.Signer), chainID, heartbeat) + var err error + heartbeat.Signature, err = pvm.Signer.Sign(tm_types.SignBytes(chainID, heartbeat)) + return err } func asTendermintSigner(signer acm.Signer) tm_types.Signer { diff --git a/consensus/tendermint/validator/sign_info.go b/consensus/tendermint/validator/sign_info.go new file mode 100644 index 00000000..12f7cbbe --- /dev/null +++ b/consensus/tendermint/validator/sign_info.go @@ -0,0 +1,240 @@ +package validator + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "time" + + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-wire/data" + "github.com/tendermint/tendermint/types" +) + +// This file is copy and pasted from (and pending): https://github.com/tendermint/tendermint/pull/1044 + +// TODO: type ? +const ( + stepNone int8 = 0 // Used to distinguish the initial state + stepPropose int8 = 1 + stepPrevote int8 = 2 + stepPrecommit int8 = 3 +) + +func voteToStep(vote *types.Vote) int8 { + switch vote.Type { + case types.VoteTypePrevote: + return stepPrevote + case types.VoteTypePrecommit: + return stepPrecommit + default: + panic("Unknown vote type") + } +} + +//------------------------------------- + +// LastSignedInfo contains information about the latest +// data signed by a validator to help prevent double signing. +type LastSignedInfo struct { + Height int64 `json:"height"` + Round int `json:"round"` + Step int8 `json:"step"` + Signature crypto.Signature `json:"signature,omitempty"` // so we dont lose signatures + SignBytes data.Bytes `json:"signbytes,omitempty"` // so we dont lose signatures +} + +func NewLastSignedInfo() *LastSignedInfo { + return &LastSignedInfo{ + Step: stepNone, + } +} + +func (info *LastSignedInfo) String() string { + return fmt.Sprintf("LH:%v, LR:%v, LS:%v", info.Height, info.Round, info.Step) +} + +// Verify returns an error if there is a height/round/step regression +// or if the HRS matches but there are no LastSignBytes. +// It returns true if HRS matches exactly and the LastSignature exists. +// It panics if the HRS matches, the LastSignBytes are not empty, but the LastSignature is empty. +func (info LastSignedInfo) Verify(height int64, round int, step int8) (bool, error) { + if info.Height > height { + return false, errors.New("Height regression") + } + + if info.Height == height { + if info.Round > round { + return false, errors.New("Round regression") + } + + if info.Round == round { + if info.Step > step { + return false, errors.New("Step regression") + } else if info.Step == step { + if info.SignBytes != nil { + if info.Signature.Empty() { + panic("info: LastSignature is nil but LastSignBytes is not!") + } + return true, nil + } + return false, errors.New("No LastSignature found") + } + } + } + return false, nil +} + +// Set height/round/step and signature on the info +func (info *LastSignedInfo) Set(height int64, round int, step int8, + signBytes []byte, sig crypto.Signature) { + + info.Height = height + info.Round = round + info.Step = step + info.Signature = sig + info.SignBytes = signBytes +} + +// Reset resets all the values. +// XXX: Unsafe. +func (info *LastSignedInfo) Reset() { + info.Height = 0 + info.Round = 0 + info.Step = 0 + info.Signature = crypto.Signature{} + info.SignBytes = nil +} + +// SignVote checks the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. +// If so, it signs the vote, updates the LastSignedInfo, and sets the signature on the vote. +// If the HRS are equal and the only thing changed is the timestamp, it sets the vote.Timestamp to the previous +// value and the Signature to the LastSignedInfo.Signature. +// Else it returns an error. +func (lsi *LastSignedInfo) SignVote(signer types.Signer, chainID string, vote *types.Vote) error { + height, round, step := vote.Height, vote.Round, voteToStep(vote) + signBytes := types.SignBytes(chainID, vote) + + sameHRS, err := lsi.Verify(height, round, step) + if err != nil { + return err + } + + // We might crash before writing to the wal, + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error + if sameHRS { + if bytes.Equal(signBytes, lsi.SignBytes) { + vote.Signature = lsi.Signature + } else if timestamp, ok := checkVotesOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { + vote.Timestamp = timestamp + vote.Signature = lsi.Signature + } else { + err = fmt.Errorf("Conflicting data") + } + return err + } + sig, err := signer.Sign(signBytes) + if err != nil { + return err + } + lsi.Set(height, round, step, signBytes, sig) + vote.Signature = sig + return nil +} + +// SignProposal checks if the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. +// If so, it signs the proposal, updates the LastSignedInfo, and sets the signature on the proposal. +// If the HRS are equal and the only thing changed is the timestamp, it sets the timestamp to the previous +// value and the Signature to the LastSignedInfo.Signature. +// Else it returns an error. +func (lsi *LastSignedInfo) SignProposal(signer types.Signer, chainID string, proposal *types.Proposal) error { + height, round, step := proposal.Height, proposal.Round, stepPropose + signBytes := types.SignBytes(chainID, proposal) + + sameHRS, err := lsi.Verify(height, round, step) + if err != nil { + return err + } + + // We might crash before writing to the wal, + // causing us to try to re-sign for the same HRS. + // If signbytes are the same, use the last signature. + // If they only differ by timestamp, use last timestamp and signature + // Otherwise, return error + if sameHRS { + if bytes.Equal(signBytes, lsi.SignBytes) { + proposal.Signature = lsi.Signature + } else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { + proposal.Timestamp = timestamp + proposal.Signature = lsi.Signature + } else { + err = fmt.Errorf("Conflicting data") + } + return err + } + sig, err := signer.Sign(signBytes) + if err != nil { + return err + } + lsi.Set(height, round, step, signBytes, sig) + proposal.Signature = sig + return nil +} + +//------------------------------------- + +// returns the timestamp from the lastSignBytes. +// returns true if the only difference in the votes is their timestamp. +func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { + var lastVote, newVote types.CanonicalJSONOnceVote + if err := json.Unmarshal(lastSignBytes, &lastVote); err != nil { + panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err)) + } + if err := json.Unmarshal(newSignBytes, &newVote); err != nil { + panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err)) + } + + lastTime, err := time.Parse(time.RFC3339Nano, lastVote.Vote.Timestamp) + if err != nil { + panic(err) + } + + // set the times to the same value and check equality + now := types.CanonicalTime(time.Now()) + lastVote.Vote.Timestamp = now + newVote.Vote.Timestamp = now + lastVoteBytes, _ := json.Marshal(lastVote) + newVoteBytes, _ := json.Marshal(newVote) + + return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes) +} + +// returns the timestamp from the lastSignBytes. +// returns true if the only difference in the proposals is their timestamp +func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { + var lastProposal, newProposal types.CanonicalJSONOnceProposal + if err := json.Unmarshal(lastSignBytes, &lastProposal); err != nil { + panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err)) + } + if err := json.Unmarshal(newSignBytes, &newProposal); err != nil { + panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err)) + } + + lastTime, err := time.Parse(time.RFC3339Nano, lastProposal.Proposal.Timestamp) + if err != nil { + panic(err) + } + + // set the times to the same value and check equality + now := types.CanonicalTime(time.Now()) + lastProposal.Proposal.Timestamp = now + newProposal.Proposal.Timestamp = now + lastProposalBytes, _ := json.Marshal(lastProposal) + newProposalBytes, _ := json.Marshal(newProposal) + + return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes) +} diff --git a/consensus/tendermint/validator/verifier.go b/consensus/tendermint/validator/verifier.go deleted file mode 100644 index 20d174dd..00000000 --- a/consensus/tendermint/validator/verifier.go +++ /dev/null @@ -1,143 +0,0 @@ -package validator - -import ( - "bytes" - "errors" - "fmt" - "sync" - - "github.com/tendermint/go-crypto" - "github.com/tendermint/go-wire/data" - tm_types "github.com/tendermint/tendermint/types" -) - -const ( - StepError = -1 - StepNone = 0 // Used to distinguish the initial state - StepPropose = 1 - StepPrevote = 2 - StepPrecommit = 3 -) - -func VoteToStep(vote *tm_types.Vote) int8 { - switch vote.Type { - case tm_types.VoteTypePrevote: - return StepPrevote - case tm_types.VoteTypePrecommit: - return StepPrecommit - default: - return StepError - } -} - -type Verifier interface { - SignVote(signer tm_types.Signer, chainID string, vote *tm_types.Vote) error - SignProposal(signer tm_types.Signer, chainID string, proposal *tm_types.Proposal) error - SignHeartbeat(signer tm_types.Signer, chainID string, heartbeat *tm_types.Heartbeat) error -} - -type LastSignedInfo struct { - mtx sync.Mutex - LastHeight int `json:"last_height"` - LastRound int `json:"last_round"` - LastStep int8 `json:"last_step"` - LastSignature crypto.Signature `json:"last_signature,omitempty"` // so we dont lose signatures - LastSignBytes data.Bytes `json:"last_signbytes,omitempty"` // so we dont lose signatures -} - -var _ Verifier = &LastSignedInfo{} - -// SignVote signs a canonical representation of the vote, along with the chainID. -// Implements PrivValidator. -func (lsi *LastSignedInfo) SignVote(signer tm_types.Signer, chainID string, vote *tm_types.Vote) error { - lsi.mtx.Lock() - defer lsi.mtx.Unlock() - signature, err := lsi.signBytesHRS(signer, vote.Height, vote.Round, VoteToStep(vote), tm_types.SignBytes(chainID, vote)) - if err != nil { - return fmt.Errorf("error signing vote: %v", err) - } - vote.Signature = signature - return nil -} - -// SignProposal signs a canonical representation of the proposal, along with the chainID. -// Implements PrivValidator. -func (lsi *LastSignedInfo) SignProposal(signer tm_types.Signer, chainID string, proposal *tm_types.Proposal) error { - lsi.mtx.Lock() - defer lsi.mtx.Unlock() - signature, err := lsi.signBytesHRS(signer, proposal.Height, proposal.Round, StepPropose, tm_types.SignBytes(chainID, proposal)) - if err != nil { - return fmt.Errorf("error signing proposal: %v", err) - } - proposal.Signature = signature - return nil -} - -// SignHeartbeat signs a canonical representation of the heartbeat, along with the chainID. -// Implements PrivValidator. -func (lsi *LastSignedInfo) SignHeartbeat(signer tm_types.Signer, chainID string, heartbeat *tm_types.Heartbeat) error { - lsi.mtx.Lock() - defer lsi.mtx.Unlock() - var err error - heartbeat.Signature, err = signer.Sign(tm_types.SignBytes(chainID, heartbeat)) - return err -} - -// signBytesHRS signs the given signBytes if the height/round/step (HRS) -// are greater than the latest state. If the HRS are equal, -// it returns the privValidator.LastSignature. -func (lsi *LastSignedInfo) signBytesHRS(signer tm_types.Signer, height, round int, step int8, signBytes []byte) (crypto.Signature, error) { - - sig := crypto.Signature{} - // If height regression, err - if lsi.LastHeight > height { - return sig, errors.New("height regression") - } - // More cases for when the height matches - if lsi.LastHeight == height { - // If round regression, err - if lsi.LastRound > round { - return sig, errors.New("round regression") - } - // If step regression, err - if lsi.LastRound == round { - if lsi.LastStep > step { - return sig, errors.New("step regression") - } else if lsi.LastStep == step { - if lsi.LastSignBytes != nil { - if lsi.LastSignature.Empty() { - return crypto.Signature{}, errors.New("lsi: LastSignature is nil but LastSignBytes is not") - } - // so we dont sign a conflicting vote or proposal - // NOTE: proposals are non-deterministic (include time), - // so we can actually lose them, but will still never sign conflicting ones - if bytes.Equal(lsi.LastSignBytes, signBytes) { - // log.Notice("Using lsi.LastSignature", "sig", lsi.LastSignature) - return lsi.LastSignature, nil - } - } - return sig, errors.New("step regression") - } - } - } - - // Sign - sig, err := signer.Sign(signBytes) - if err != nil { - return sig, err - } - - // Persist height/round/step - lsi.LastHeight = height - lsi.LastRound = round - lsi.LastStep = step - lsi.LastSignature = sig - lsi.LastSignBytes = signBytes - - return sig, nil -} - -func (lsi *LastSignedInfo) String() string { - return fmt.Sprintf("LastSignedInfo{LastHeight:%v, LastRound:%v, LastStep:%v}", - lsi.LastHeight, lsi.LastRound, lsi.LastStep) -} diff --git a/genesis/genesis.go b/genesis/genesis.go index 7d62594e..1c4d0341 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -31,10 +31,6 @@ import ( // of replay attack between chains with the same name. const ShortHashSuffixBytes = 3 -// we store the GenesisDoc in the db under this key - -var GenDocKey = []byte("GenDocKey") - //------------------------------------------------------------ // core types for a genesis definition -- GitLab