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