github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/privval/file.go (about)

     1  package privval
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"time"
     9  
    10  	"github.com/gnolang/gno/tm2/pkg/amino"
    11  	"github.com/gnolang/gno/tm2/pkg/bft/types"
    12  	tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time"
    13  	"github.com/gnolang/gno/tm2/pkg/crypto"
    14  	"github.com/gnolang/gno/tm2/pkg/crypto/ed25519"
    15  	osm "github.com/gnolang/gno/tm2/pkg/os"
    16  )
    17  
    18  // TODO: type ?
    19  const (
    20  	stepNone      int8 = 0 // Used to distinguish the initial state
    21  	stepPropose   int8 = 1
    22  	stepPrevote   int8 = 2
    23  	stepPrecommit int8 = 3
    24  )
    25  
    26  // A vote is either stepPrevote or stepPrecommit.
    27  func voteToStep(vote *types.Vote) int8 {
    28  	switch vote.Type {
    29  	case types.PrevoteType:
    30  		return stepPrevote
    31  	case types.PrecommitType:
    32  		return stepPrecommit
    33  	default:
    34  		panic("Unknown vote type")
    35  	}
    36  }
    37  
    38  // -------------------------------------------------------------------------------
    39  
    40  // FilePVKey stores the immutable part of PrivValidator.
    41  type FilePVKey struct {
    42  	Address types.Address  `json:"address"`
    43  	PubKey  crypto.PubKey  `json:"pub_key"`
    44  	PrivKey crypto.PrivKey `json:"priv_key"`
    45  
    46  	filePath string
    47  }
    48  
    49  // Save persists the FilePVKey to its filePath.
    50  func (pvKey FilePVKey) Save() {
    51  	outFile := pvKey.filePath
    52  	if outFile == "" {
    53  		panic("cannot save PrivValidator key: filePath not set")
    54  	}
    55  
    56  	jsonBytes, err := amino.MarshalJSONIndent(pvKey, "", "  ")
    57  	if err != nil {
    58  		panic(err)
    59  	}
    60  	err = osm.WriteFileAtomic(outFile, jsonBytes, 0o600)
    61  	if err != nil {
    62  		panic(err)
    63  	}
    64  }
    65  
    66  // -------------------------------------------------------------------------------
    67  
    68  // FilePVLastSignState stores the mutable part of PrivValidator.
    69  type FilePVLastSignState struct {
    70  	Height    int64  `json:"height"`
    71  	Round     int    `json:"round"`
    72  	Step      int8   `json:"step"`
    73  	Signature []byte `json:"signature,omitempty"`
    74  	SignBytes []byte `json:"signbytes,omitempty"`
    75  
    76  	filePath string
    77  }
    78  
    79  // CheckHRS checks the given height, round, step (HRS) against that of the
    80  // FilePVLastSignState. It returns an error if the arguments constitute a regression,
    81  // or if they match but the SignBytes are empty.
    82  // The returned boolean indicates whether the last Signature should be reused -
    83  // it returns true if the HRS matches the arguments and the SignBytes are not empty (indicating
    84  // we have already signed for this HRS, and can reuse the existing signature).
    85  // It panics if the HRS matches the arguments, there's a SignBytes, but no Signature.
    86  func (lss *FilePVLastSignState) CheckHRS(height int64, round int, step int8) (bool, error) {
    87  	if lss.Height > height {
    88  		return false, fmt.Errorf("height regression. Got %v, last height %v", height, lss.Height)
    89  	}
    90  
    91  	if lss.Height == height {
    92  		if lss.Round > round {
    93  			return false, fmt.Errorf("round regression at height %v. Got %v, last round %v", height, round, lss.Round)
    94  		}
    95  
    96  		if lss.Round == round {
    97  			if lss.Step > step {
    98  				return false, fmt.Errorf("step regression at height %v round %v. Got %v, last step %v", height, round, step, lss.Step)
    99  			} else if lss.Step == step {
   100  				if lss.SignBytes != nil {
   101  					if lss.Signature == nil {
   102  						panic("pv: Signature is nil but SignBytes is not!")
   103  					}
   104  					return true, nil
   105  				}
   106  				return false, errors.New("no SignBytes found")
   107  			}
   108  		}
   109  	}
   110  	return false, nil
   111  }
   112  
   113  // Save persists the FilePvLastSignState to its filePath.
   114  func (lss *FilePVLastSignState) Save() {
   115  	outFile := lss.filePath
   116  	if outFile == "" {
   117  		panic("cannot save FilePVLastSignState: filePath not set")
   118  	}
   119  	jsonBytes, err := amino.MarshalJSONIndent(lss, "", "  ")
   120  	if err != nil {
   121  		panic(err)
   122  	}
   123  	err = osm.WriteFileAtomic(outFile, jsonBytes, 0o600)
   124  	if err != nil {
   125  		panic(err)
   126  	}
   127  }
   128  
   129  // -------------------------------------------------------------------------------
   130  
   131  // FilePV implements PrivValidator using data persisted to disk
   132  // to prevent double signing.
   133  // NOTE: the directories containing pv.Key.filePath and pv.LastSignState.filePath must already exist.
   134  // It includes the LastSignature and LastSignBytes so we don't lose the signature
   135  // if the process crashes after signing but before the resulting consensus message is processed.
   136  type FilePV struct {
   137  	Key           FilePVKey
   138  	LastSignState FilePVLastSignState
   139  }
   140  
   141  // GenFilePV generates a new validator with randomly generated private key
   142  // and sets the filePaths, but does not call Save().
   143  func GenFilePV(keyFilePath, stateFilePath string) *FilePV {
   144  	privKey := ed25519.GenPrivKey()
   145  
   146  	return &FilePV{
   147  		Key: FilePVKey{
   148  			Address:  privKey.PubKey().Address(),
   149  			PubKey:   privKey.PubKey(),
   150  			PrivKey:  privKey,
   151  			filePath: keyFilePath,
   152  		},
   153  		LastSignState: FilePVLastSignState{
   154  			Step:     stepNone,
   155  			filePath: stateFilePath,
   156  		},
   157  	}
   158  }
   159  
   160  // LoadFilePV loads a FilePV from the filePaths.  The FilePV handles double
   161  // signing prevention by persisting data to the stateFilePath.  If either file path
   162  // does not exist, the program will exit.
   163  func LoadFilePV(keyFilePath, stateFilePath string) *FilePV {
   164  	return loadFilePV(keyFilePath, stateFilePath, true)
   165  }
   166  
   167  // LoadFilePVEmptyState loads a FilePV from the given keyFilePath, with an empty LastSignState.
   168  // If the keyFilePath does not exist, the program will exit.
   169  func LoadFilePVEmptyState(keyFilePath, stateFilePath string) *FilePV {
   170  	return loadFilePV(keyFilePath, stateFilePath, false)
   171  }
   172  
   173  // If loadState is true, we load from the stateFilePath. Otherwise, we use an empty LastSignState.
   174  func loadFilePV(keyFilePath, stateFilePath string, loadState bool) *FilePV {
   175  	keyJSONBytes, err := os.ReadFile(keyFilePath)
   176  	if err != nil {
   177  		osm.Exit(err.Error())
   178  	}
   179  	pvKey := FilePVKey{}
   180  	err = amino.UnmarshalJSON(keyJSONBytes, &pvKey)
   181  	if err != nil {
   182  		osm.Exit(fmt.Sprintf("Error reading PrivValidator key from %v: %v\n", keyFilePath, err))
   183  	}
   184  
   185  	// overwrite pubkey and address for convenience
   186  	pvKey.PubKey = pvKey.PrivKey.PubKey()
   187  	pvKey.Address = pvKey.PubKey.Address()
   188  	pvKey.filePath = keyFilePath
   189  
   190  	pvState := FilePVLastSignState{}
   191  	if loadState {
   192  		stateJSONBytes, err := os.ReadFile(stateFilePath)
   193  		if err != nil {
   194  			osm.Exit(err.Error())
   195  		}
   196  		err = amino.UnmarshalJSON(stateJSONBytes, &pvState)
   197  		if err != nil {
   198  			osm.Exit(fmt.Sprintf("Error reading PrivValidator state from %v: %v\n", stateFilePath, err))
   199  		}
   200  	}
   201  
   202  	pvState.filePath = stateFilePath
   203  
   204  	return &FilePV{
   205  		Key:           pvKey,
   206  		LastSignState: pvState,
   207  	}
   208  }
   209  
   210  // LoadOrGenFilePV loads a FilePV from the given filePaths
   211  // or else generates a new one and saves it to the filePaths.
   212  func LoadOrGenFilePV(keyFilePath, stateFilePath string) *FilePV {
   213  	var pv *FilePV
   214  	if osm.FileExists(keyFilePath) {
   215  		pv = LoadFilePV(keyFilePath, stateFilePath)
   216  	} else {
   217  		pv = GenFilePV(keyFilePath, stateFilePath)
   218  		pv.Save()
   219  	}
   220  	return pv
   221  }
   222  
   223  // GetAddress returns the address of the validator.
   224  // Implements PrivValidator.
   225  func (pv *FilePV) GetAddress() types.Address {
   226  	return pv.Key.Address
   227  }
   228  
   229  // GetPubKey returns the public key of the validator.
   230  // Implements PrivValidator.
   231  func (pv *FilePV) GetPubKey() crypto.PubKey {
   232  	return pv.Key.PubKey
   233  }
   234  
   235  // SignVote signs a canonical representation of the vote, along with the
   236  // chainID. Implements PrivValidator.
   237  func (pv *FilePV) SignVote(chainID string, vote *types.Vote) error {
   238  	if err := pv.signVote(chainID, vote); err != nil {
   239  		return fmt.Errorf("error signing vote: %w", err)
   240  	}
   241  	return nil
   242  }
   243  
   244  // SignProposal signs a canonical representation of the proposal, along with
   245  // the chainID. Implements PrivValidator.
   246  func (pv *FilePV) SignProposal(chainID string, proposal *types.Proposal) error {
   247  	if err := pv.signProposal(chainID, proposal); err != nil {
   248  		return fmt.Errorf("error signing proposal: %w", err)
   249  	}
   250  	return nil
   251  }
   252  
   253  // Save persists the FilePV to disk.
   254  func (pv *FilePV) Save() {
   255  	pv.Key.Save()
   256  	pv.LastSignState.Save()
   257  }
   258  
   259  // Reset resets all fields in the FilePV.
   260  // NOTE: Unsafe!
   261  func (pv *FilePV) Reset() {
   262  	var sig []byte
   263  	pv.LastSignState.Height = 0
   264  	pv.LastSignState.Round = 0
   265  	pv.LastSignState.Step = 0
   266  	pv.LastSignState.Signature = sig
   267  	pv.LastSignState.SignBytes = nil
   268  	pv.Save()
   269  }
   270  
   271  // String returns a string representation of the FilePV.
   272  func (pv *FilePV) String() string {
   273  	return fmt.Sprintf("PrivValidator{%v LH:%v, LR:%v, LS:%v}", pv.GetAddress(), pv.LastSignState.Height, pv.LastSignState.Round, pv.LastSignState.Step)
   274  }
   275  
   276  // ------------------------------------------------------------------------------------
   277  
   278  // signVote checks if the vote is good to sign and sets the vote signature.
   279  // It may need to set the timestamp as well if the vote is otherwise the same as
   280  // a previously signed vote (ie. we crashed after signing but before the vote hit the WAL).
   281  func (pv *FilePV) signVote(chainID string, vote *types.Vote) error {
   282  	height, round, step := vote.Height, vote.Round, voteToStep(vote)
   283  
   284  	lss := pv.LastSignState
   285  
   286  	sameHRS, err := lss.CheckHRS(height, round, step)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	signBytes := vote.SignBytes(chainID)
   292  
   293  	// We might crash before writing to the wal,
   294  	// causing us to try to re-sign for the same HRS.
   295  	// If signbytes are the same, use the last signature.
   296  	// If they only differ by timestamp, use last timestamp and signature
   297  	// Otherwise, return error
   298  	if sameHRS {
   299  		if bytes.Equal(signBytes, lss.SignBytes) {
   300  			vote.Signature = lss.Signature
   301  		} else if timestamp, ok := checkVotesOnlyDifferByTimestamp(lss.SignBytes, signBytes); ok {
   302  			vote.Timestamp = timestamp
   303  			vote.Signature = lss.Signature
   304  		} else {
   305  			err = fmt.Errorf("conflicting data")
   306  		}
   307  		return err
   308  	}
   309  
   310  	// It passed the checks. Sign the vote
   311  	sig, err := pv.Key.PrivKey.Sign(signBytes)
   312  	if err != nil {
   313  		return err
   314  	}
   315  	pv.saveSigned(height, round, step, signBytes, sig)
   316  	vote.Signature = sig
   317  	return nil
   318  }
   319  
   320  // signProposal checks if the proposal is good to sign and sets the proposal signature.
   321  // It may need to set the timestamp as well if the proposal is otherwise the same as
   322  // a previously signed proposal ie. we crashed after signing but before the proposal hit the WAL).
   323  func (pv *FilePV) signProposal(chainID string, proposal *types.Proposal) error {
   324  	height, round, step := proposal.Height, proposal.Round, stepPropose
   325  
   326  	lss := pv.LastSignState
   327  
   328  	sameHRS, err := lss.CheckHRS(height, round, step)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	signBytes := proposal.SignBytes(chainID)
   334  
   335  	// We might crash before writing to the wal,
   336  	// causing us to try to re-sign for the same HRS.
   337  	// If signbytes are the same, use the last signature.
   338  	// If they only differ by timestamp, use last timestamp and signature
   339  	// Otherwise, return error
   340  	if sameHRS {
   341  		if bytes.Equal(signBytes, lss.SignBytes) {
   342  			proposal.Signature = lss.Signature
   343  		} else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(lss.SignBytes, signBytes); ok {
   344  			proposal.Timestamp = timestamp
   345  			proposal.Signature = lss.Signature
   346  		} else {
   347  			err = fmt.Errorf("conflicting data")
   348  		}
   349  		return err
   350  	}
   351  
   352  	// It passed the checks. Sign the proposal
   353  	sig, err := pv.Key.PrivKey.Sign(signBytes)
   354  	if err != nil {
   355  		return err
   356  	}
   357  	pv.saveSigned(height, round, step, signBytes, sig)
   358  	proposal.Signature = sig
   359  	return nil
   360  }
   361  
   362  // Persist height/round/step and signature
   363  func (pv *FilePV) saveSigned(height int64, round int, step int8,
   364  	signBytes []byte, sig []byte,
   365  ) {
   366  	pv.LastSignState.Height = height
   367  	pv.LastSignState.Round = round
   368  	pv.LastSignState.Step = step
   369  	pv.LastSignState.Signature = sig
   370  	pv.LastSignState.SignBytes = signBytes
   371  	pv.LastSignState.Save()
   372  }
   373  
   374  // -----------------------------------------------------------------------------------------
   375  
   376  // returns the timestamp from the lastSignBytes.
   377  // returns true if the only difference in the votes is their timestamp.
   378  func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) {
   379  	var lastVote, newVote types.CanonicalVote
   380  	if err := amino.UnmarshalSized(lastSignBytes, &lastVote); err != nil {
   381  		panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err))
   382  	}
   383  	if err := amino.UnmarshalSized(newSignBytes, &newVote); err != nil {
   384  		panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err))
   385  	}
   386  
   387  	lastTime := lastVote.Timestamp
   388  
   389  	// set the times to the same value and check equality
   390  	now := tmtime.Now()
   391  	lastVote.Timestamp = now
   392  	newVote.Timestamp = now
   393  	lastVoteBytes, _ := amino.MarshalJSON(lastVote)
   394  	newVoteBytes, _ := amino.MarshalJSON(newVote)
   395  
   396  	return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes)
   397  }
   398  
   399  // returns the timestamp from the lastSignBytes.
   400  // returns true if the only difference in the proposals is their timestamp
   401  func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) {
   402  	var lastProposal, newProposal types.CanonicalProposal
   403  	if err := amino.UnmarshalSized(lastSignBytes, &lastProposal); err != nil {
   404  		panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err))
   405  	}
   406  	if err := amino.UnmarshalSized(newSignBytes, &newProposal); err != nil {
   407  		panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err))
   408  	}
   409  
   410  	lastTime := lastProposal.Timestamp
   411  	// set the times to the same value and check equality
   412  	now := tmtime.Now()
   413  	lastProposal.Timestamp = now
   414  	newProposal.Timestamp = now
   415  	lastProposalBytes, _ := amino.MarshalSized(lastProposal)
   416  	newProposalBytes, _ := amino.MarshalSized(newProposal)
   417  
   418  	return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes)
   419  }