package privval import ( "bytes" "context" "encoding/json" "errors" "fmt" "os" "time" "github.com/gogo/protobuf/proto" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/internal/libs/protoio" "github.com/tendermint/tendermint/internal/libs/tempfile" tmbytes "github.com/tendermint/tendermint/libs/bytes" tmjson "github.com/tendermint/tendermint/libs/json" tmos "github.com/tendermint/tendermint/libs/os" tmtime "github.com/tendermint/tendermint/libs/time" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/types" ) // TODO: type ? const ( stepNone int8 = 0 // Used to distinguish the initial state stepPropose int8 = 1 stepPrevote int8 = 2 stepPrecommit int8 = 3 ) // A vote is either stepPrevote or stepPrecommit. func voteToStep(vote *tmproto.Vote) (int8, error) { switch vote.Type { case tmproto.PrevoteType: return stepPrevote, nil case tmproto.PrecommitType: return stepPrecommit, nil default: return 0, fmt.Errorf("unknown vote type: %v", vote.Type) } } //------------------------------------------------------------------------------- // FilePVKey stores the immutable part of PrivValidator. type FilePVKey struct { Address types.Address `json:"address"` PubKey crypto.PubKey `json:"pub_key"` PrivKey crypto.PrivKey `json:"priv_key"` filePath string } // Save persists the FilePVKey to its filePath. func (pvKey FilePVKey) Save() error { outFile := pvKey.filePath if outFile == "" { return errors.New("cannot save PrivValidator key: filePath not set") } jsonBytes, err := tmjson.MarshalIndent(pvKey, "", " ") if err != nil { return err } return tempfile.WriteFileAtomic(outFile, jsonBytes, 0600) } //------------------------------------------------------------------------------- // FilePVLastSignState stores the mutable part of PrivValidator. type FilePVLastSignState struct { Height int64 `json:"height,string"` Round int32 `json:"round"` Step int8 `json:"step"` Signature []byte `json:"signature,omitempty"` SignBytes tmbytes.HexBytes `json:"signbytes,omitempty"` filePath string } // CheckHRS checks the given height, round, step (HRS) against that of the // FilePVLastSignState. It returns an error if the arguments constitute a regression, // or if they match but the SignBytes are empty. // The returned boolean indicates whether the last Signature should be reused - // it returns true if the HRS matches the arguments and the SignBytes are not empty (indicating // we have already signed for this HRS, and can reuse the existing signature). // It panics if the HRS matches the arguments, there's a SignBytes, but no Signature. func (lss *FilePVLastSignState) CheckHRS(height int64, round int32, step int8) (bool, error) { if lss.Height > height { return false, fmt.Errorf("height regression. Got %v, last height %v", height, lss.Height) } if lss.Height == height { if lss.Round > round { return false, fmt.Errorf("round regression at height %v. Got %v, last round %v", height, round, lss.Round) } if lss.Round == round { if lss.Step > step { return false, fmt.Errorf( "step regression at height %v round %v. Got %v, last step %v", height, round, step, lss.Step, ) } else if lss.Step == step { if lss.SignBytes != nil { if lss.Signature == nil { panic("pv: Signature is nil but SignBytes is not!") } return true, nil } return false, errors.New("no SignBytes found") } } } return false, nil } // Save persists the FilePvLastSignState to its filePath. func (lss *FilePVLastSignState) Save() error { outFile := lss.filePath if outFile == "" { return errors.New("cannot save FilePVLastSignState: filePath not set") } jsonBytes, err := json.MarshalIndent(lss, "", " ") if err != nil { return err } return tempfile.WriteFileAtomic(outFile, jsonBytes, 0600) } //------------------------------------------------------------------------------- // FilePV implements PrivValidator using data persisted to disk // to prevent double signing. // NOTE: the directories containing pv.Key.filePath and pv.LastSignState.filePath must already exist. // It includes the LastSignature and LastSignBytes so we don't lose the signature // if the process crashes after signing but before the resulting consensus message is processed. type FilePV struct { Key FilePVKey LastSignState FilePVLastSignState } var _ types.PrivValidator = (*FilePV)(nil) // NewFilePV generates a new validator from the given key and paths. func NewFilePV(privKey crypto.PrivKey, keyFilePath, stateFilePath string) *FilePV { return &FilePV{ Key: FilePVKey{ Address: privKey.PubKey().Address(), PubKey: privKey.PubKey(), PrivKey: privKey, filePath: keyFilePath, }, LastSignState: FilePVLastSignState{ Step: stepNone, filePath: stateFilePath, }, } } // GenFilePV generates a new validator with randomly generated private key // and sets the filePaths, but does not call Save(). func GenFilePV(keyFilePath, stateFilePath, keyType string) (*FilePV, error) { switch keyType { case types.ABCIPubKeyTypeSecp256k1: return NewFilePV(secp256k1.GenPrivKey(), keyFilePath, stateFilePath), nil case "", types.ABCIPubKeyTypeEd25519: return NewFilePV(ed25519.GenPrivKey(), keyFilePath, stateFilePath), nil default: return nil, fmt.Errorf("key type: %s is not supported", keyType) } } // LoadFilePV loads a FilePV from the filePaths. The FilePV handles double // signing prevention by persisting data to the stateFilePath. If either file path // does not exist, the program will exit. func LoadFilePV(keyFilePath, stateFilePath string) (*FilePV, error) { return loadFilePV(keyFilePath, stateFilePath, true) } // LoadFilePVEmptyState loads a FilePV from the given keyFilePath, with an empty LastSignState. // If the keyFilePath does not exist, the program will exit. func LoadFilePVEmptyState(keyFilePath, stateFilePath string) (*FilePV, error) { return loadFilePV(keyFilePath, stateFilePath, false) } // If loadState is true, we load from the stateFilePath. Otherwise, we use an empty LastSignState. func loadFilePV(keyFilePath, stateFilePath string, loadState bool) (*FilePV, error) { keyJSONBytes, err := os.ReadFile(keyFilePath) if err != nil { return nil, err } pvKey := FilePVKey{} err = tmjson.Unmarshal(keyJSONBytes, &pvKey) if err != nil { return nil, fmt.Errorf("error reading PrivValidator key from %v: %w", keyFilePath, err) } // overwrite pubkey and address for convenience pvKey.PubKey = pvKey.PrivKey.PubKey() pvKey.Address = pvKey.PubKey.Address() pvKey.filePath = keyFilePath pvState := FilePVLastSignState{} if loadState { stateJSONBytes, err := os.ReadFile(stateFilePath) if err != nil { return nil, err } err = json.Unmarshal(stateJSONBytes, &pvState) if err != nil { return nil, fmt.Errorf("error reading PrivValidator state from %v: %w", stateFilePath, err) } } pvState.filePath = stateFilePath return &FilePV{ Key: pvKey, LastSignState: pvState, }, nil } // LoadOrGenFilePV loads a FilePV from the given filePaths // or else generates a new one and saves it to the filePaths. func LoadOrGenFilePV(keyFilePath, stateFilePath string) (*FilePV, error) { if tmos.FileExists(keyFilePath) { pv, err := LoadFilePV(keyFilePath, stateFilePath) if err != nil { return nil, err } return pv, nil } pv, err := GenFilePV(keyFilePath, stateFilePath, "") if err != nil { return nil, err } if err := pv.Save(); err != nil { return nil, err } return pv, nil } // GetAddress returns the address of the validator. // Implements PrivValidator. func (pv *FilePV) GetAddress() types.Address { return pv.Key.Address } // GetPubKey returns the public key of the validator. // Implements PrivValidator. func (pv *FilePV) GetPubKey(ctx context.Context) (crypto.PubKey, error) { return pv.Key.PubKey, nil } // SignVote signs a canonical representation of the vote, along with the // chainID. Implements PrivValidator. func (pv *FilePV) SignVote(ctx context.Context, chainID string, vote *tmproto.Vote) error { if err := pv.signVote(chainID, vote); err != nil { return fmt.Errorf("error signing vote: %w", err) } return nil } // SignProposal signs a canonical representation of the proposal, along with // the chainID. Implements PrivValidator. func (pv *FilePV) SignProposal(ctx context.Context, chainID string, proposal *tmproto.Proposal) error { if err := pv.signProposal(chainID, proposal); err != nil { return fmt.Errorf("error signing proposal: %w", err) } return nil } // Save persists the FilePV to disk. func (pv *FilePV) Save() error { if err := pv.Key.Save(); err != nil { return err } return pv.LastSignState.Save() } // Reset resets all fields in the FilePV. // NOTE: Unsafe! func (pv *FilePV) Reset() error { var sig []byte pv.LastSignState.Height = 0 pv.LastSignState.Round = 0 pv.LastSignState.Step = 0 pv.LastSignState.Signature = sig pv.LastSignState.SignBytes = nil return pv.Save() } // String returns a string representation of the FilePV. func (pv *FilePV) String() string { return fmt.Sprintf( "PrivValidator{%v LH:%v, LR:%v, LS:%v}", pv.GetAddress(), pv.LastSignState.Height, pv.LastSignState.Round, pv.LastSignState.Step, ) } //------------------------------------------------------------------------------------ // signVote checks if the vote is good to sign and sets the vote signature. // It may need to set the timestamp as well if the vote is otherwise the same as // a previously signed vote (ie. we crashed after signing but before the vote hit the WAL). func (pv *FilePV) signVote(chainID string, vote *tmproto.Vote) error { step, err := voteToStep(vote) if err != nil { return err } height := vote.Height round := vote.Round lss := pv.LastSignState sameHRS, err := lss.CheckHRS(height, round, step) if err != nil { return err } signBytes := types.VoteSignBytes(chainID, vote) // 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, lss.SignBytes) { vote.Signature = lss.Signature } else { timestamp, ok, err := checkVotesOnlyDifferByTimestamp(lss.SignBytes, signBytes) if err != nil { return err } if !ok { return errors.New("conflicting data") } vote.Timestamp = timestamp vote.Signature = lss.Signature return nil } } // It passed the checks. Sign the vote sig, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { return err } if err := pv.saveSigned(height, round, step, signBytes, sig); err != nil { return err } vote.Signature = sig return nil } // signProposal checks if the proposal is good to sign and sets the proposal signature. // It may need to set the timestamp as well if the proposal is otherwise the same as // a previously signed proposal ie. we crashed after signing but before the proposal hit the WAL). func (pv *FilePV) signProposal(chainID string, proposal *tmproto.Proposal) error { height, round, step := proposal.Height, proposal.Round, stepPropose lss := pv.LastSignState sameHRS, err := lss.CheckHRS(height, round, step) if err != nil { return err } signBytes := types.ProposalSignBytes(chainID, proposal) // 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, lss.SignBytes) { proposal.Signature = lss.Signature } else { timestamp, ok, err := checkProposalsOnlyDifferByTimestamp(lss.SignBytes, signBytes) if err != nil { return err } if !ok { return errors.New("conflicting data") } proposal.Timestamp = timestamp proposal.Signature = lss.Signature return nil } } // It passed the checks. Sign the proposal sig, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { return err } if err := pv.saveSigned(height, round, step, signBytes, sig); err != nil { return err } proposal.Signature = sig return nil } // Persist height/round/step and signature func (pv *FilePV) saveSigned(height int64, round int32, step int8, signBytes []byte, sig []byte) error { pv.LastSignState.Height = height pv.LastSignState.Round = round pv.LastSignState.Step = step pv.LastSignState.Signature = sig pv.LastSignState.SignBytes = signBytes return pv.LastSignState.Save() } //----------------------------------------------------------------------------------------- // 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, error) { var lastVote, newVote tmproto.CanonicalVote if err := protoio.UnmarshalDelimited(lastSignBytes, &lastVote); err != nil { return time.Time{}, false, fmt.Errorf("LastSignBytes cannot be unmarshalled into vote: %w", err) } if err := protoio.UnmarshalDelimited(newSignBytes, &newVote); err != nil { return time.Time{}, false, fmt.Errorf("signBytes cannot be unmarshalled into vote: %w", err) } lastTime := lastVote.Timestamp // set the times to the same value and check equality now := tmtime.Now() lastVote.Timestamp = now newVote.Timestamp = now return lastTime, proto.Equal(&newVote, &lastVote), nil } // 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, error) { var lastProposal, newProposal tmproto.CanonicalProposal if err := protoio.UnmarshalDelimited(lastSignBytes, &lastProposal); err != nil { return time.Time{}, false, fmt.Errorf("LastSignBytes cannot be unmarshalled into proposal: %w", err) } if err := protoio.UnmarshalDelimited(newSignBytes, &newProposal); err != nil { return time.Time{}, false, fmt.Errorf("signBytes cannot be unmarshalled into proposal: %w", err) } lastTime := lastProposal.Timestamp // set the times to the same value and check equality now := tmtime.Now() lastProposal.Timestamp = now newProposal.Timestamp = now return lastTime, proto.Equal(&newProposal, &lastProposal), nil }