You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

238 lines
7.5 KiB

package types
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
crypto "github.com/tendermint/go-crypto"
"github.com/tendermint/tendermint/types"
cmn "github.com/tendermint/tmlibs/common"
)
// 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 cmn.HexBytes `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 := vote.SignBytes(chainID)
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 := proposal.SignBytes(chainID)
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(types.TimeFormat, 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(types.TimeFormat, 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)
}