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 }