diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index b9a5454c..26021918 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -34,6 +34,7 @@ program](https://hackerone.com/tendermint). reset to 0 ### FEATURES: +- [privval] \#1181 Split immutable and mutable parts of priv_validator.json ### IMPROVEMENTS: diff --git a/cmd/priv_val_server/main.go b/cmd/priv_val_server/main.go index 03aa57f4..54602558 100644 --- a/cmd/priv_val_server/main.go +++ b/cmd/priv_val_server/main.go @@ -13,9 +13,10 @@ import ( func main() { var ( - addr = flag.String("addr", ":26659", "Address of client to connect to") - chainID = flag.String("chain-id", "mychain", "chain id") - privValPath = flag.String("priv", "", "priv val file path") + addr = flag.String("addr", ":26659", "Address of client to connect to") + chainID = flag.String("chain-id", "mychain", "chain id") + privValKeyPath = flag.String("priv-key", "", "priv val key file path") + privValStatePath = flag.String("priv-state", "", "priv val state file path") logger = log.NewTMLogger( log.NewSyncWriter(os.Stdout), @@ -27,10 +28,11 @@ func main() { "Starting private validator", "addr", *addr, "chainID", *chainID, - "privPath", *privValPath, + "privKeyPath", *privValKeyPath, + "privStatePath", *privValStatePath, ) - pv := privval.LoadFilePV(*privValPath) + pv := privval.LoadFilePV(*privValKeyPath, *privValStatePath) rs := privval.NewRemoteSigner( logger, diff --git a/cmd/tendermint/commands/gen_validator.go b/cmd/tendermint/commands/gen_validator.go index 20d43d4d..572bc974 100644 --- a/cmd/tendermint/commands/gen_validator.go +++ b/cmd/tendermint/commands/gen_validator.go @@ -17,7 +17,7 @@ var GenValidatorCmd = &cobra.Command{ } func genValidator(cmd *cobra.Command, args []string) { - pv := privval.GenFilePV("") + pv := privval.GenFilePV("", "") jsbz, err := cdc.MarshalJSON(pv) if err != nil { panic(err) diff --git a/cmd/tendermint/commands/init.go b/cmd/tendermint/commands/init.go index 85ee4491..0ef1e2d2 100644 --- a/cmd/tendermint/commands/init.go +++ b/cmd/tendermint/commands/init.go @@ -26,15 +26,18 @@ func initFiles(cmd *cobra.Command, args []string) error { func initFilesWithConfig(config *cfg.Config) error { // private validator - privValFile := config.PrivValidatorFile() + privValkeyFile := config.PrivValidatorKeyFile() + privValStateFile := config.PrivValidatorStateFile() var pv *privval.FilePV - if cmn.FileExists(privValFile) { - pv = privval.LoadFilePV(privValFile) - logger.Info("Found private validator", "path", privValFile) + if cmn.FileExists(privValkeyFile) { + pv = privval.LoadFilePV(privValkeyFile, privValStateFile) + logger.Info("Found private validator", "keyFile", privValkeyFile, + "stateFile", privValStateFile) } else { - pv = privval.GenFilePV(privValFile) + pv = privval.GenFilePV(privValkeyFile, privValStateFile) pv.Save() - logger.Info("Generated private validator", "path", privValFile) + logger.Info("Generated private validator", "keyFile", privValkeyFile, + "stateFile", privValStateFile) } nodeKeyFile := config.NodeKeyFile() diff --git a/cmd/tendermint/commands/reset_priv_validator.go b/cmd/tendermint/commands/reset_priv_validator.go index 53d34712..1669e673 100644 --- a/cmd/tendermint/commands/reset_priv_validator.go +++ b/cmd/tendermint/commands/reset_priv_validator.go @@ -27,19 +27,20 @@ var ResetPrivValidatorCmd = &cobra.Command{ // XXX: this is totally unsafe. // it's only suitable for testnets. func resetAll(cmd *cobra.Command, args []string) { - ResetAll(config.DBDir(), config.P2P.AddrBookFile(), config.PrivValidatorFile(), logger) + ResetAll(config.DBDir(), config.P2P.AddrBookFile(), config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), logger) } // XXX: this is totally unsafe. // it's only suitable for testnets. func resetPrivValidator(cmd *cobra.Command, args []string) { - resetFilePV(config.PrivValidatorFile(), logger) + resetFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile(), logger) } // ResetAll removes the privValidator and address book files plus all data. // Exported so other CLI tools can use it. -func ResetAll(dbDir, addrBookFile, privValFile string, logger log.Logger) { - resetFilePV(privValFile, logger) +func ResetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile string, logger log.Logger) { + resetFilePV(privValKeyFile, privValStateFile, logger) removeAddrBook(addrBookFile, logger) if err := os.RemoveAll(dbDir); err == nil { logger.Info("Removed all blockchain history", "dir", dbDir) @@ -48,15 +49,17 @@ func ResetAll(dbDir, addrBookFile, privValFile string, logger log.Logger) { } } -func resetFilePV(privValFile string, logger log.Logger) { - if _, err := os.Stat(privValFile); err == nil { - pv := privval.LoadFilePV(privValFile) +func resetFilePV(privValKeyFile, privValStateFile string, logger log.Logger) { + if _, err := os.Stat(privValKeyFile); err == nil { + pv := privval.LoadFilePV(privValKeyFile, privValStateFile) pv.Reset() - logger.Info("Reset private validator file to genesis state", "file", privValFile) + logger.Info("Reset private validator file to genesis state", "keyFile", privValKeyFile, + "stateFile", privValStateFile) } else { - pv := privval.GenFilePV(privValFile) + pv := privval.GenFilePV(privValKeyFile, privValStateFile) pv.Save() - logger.Info("Generated private validator file", "file", privValFile) + logger.Info("Generated private validator file", "file", "keyFile", privValKeyFile, + "stateFile", privValStateFile) } } diff --git a/cmd/tendermint/commands/show_validator.go b/cmd/tendermint/commands/show_validator.go index 54765164..78bc0603 100644 --- a/cmd/tendermint/commands/show_validator.go +++ b/cmd/tendermint/commands/show_validator.go @@ -16,7 +16,7 @@ var ShowValidatorCmd = &cobra.Command{ } func showValidator(cmd *cobra.Command, args []string) { - privValidator := privval.LoadOrGenFilePV(config.PrivValidatorFile()) + privValidator := privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) pubKeyJSONBytes, _ := cdc.MarshalJSON(privValidator.GetPubKey()) fmt.Println(string(pubKeyJSONBytes)) } diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index 0f7dd79a..6398f4eb 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -88,8 +88,10 @@ func testnetFiles(cmd *cobra.Command, args []string) error { initFilesWithConfig(config) - pvFile := filepath.Join(nodeDir, config.BaseConfig.PrivValidator) - pv := privval.LoadFilePV(pvFile) + pvKeyFile := filepath.Join(nodeDir, config.BaseConfig.PrivValidatorKey) + pvStateFile := filepath.Join(nodeDir, config.BaseConfig.PrivValidatorState) + + pv := privval.LoadFilePV(pvKeyFile, pvStateFile) genVals[i] = types.GenesisValidator{ Address: pv.GetPubKey().Address(), PubKey: pv.GetPubKey(), diff --git a/config/config.go b/config/config.go index 23b03399..85d55b55 100644 --- a/config/config.go +++ b/config/config.go @@ -35,15 +35,19 @@ var ( defaultConfigFileName = "config.toml" defaultGenesisJSONName = "genesis.json" - defaultPrivValName = "priv_validator.json" + defaultPrivValKeyName = "priv_validator_key.json" + defaultPrivValStateName = "priv_validator_state.json" + defaultNodeKeyName = "node_key.json" defaultAddrBookName = "addrbook.json" - defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName) - defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName) - defaultPrivValPath = filepath.Join(defaultConfigDir, defaultPrivValName) - defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName) - defaultAddrBookPath = filepath.Join(defaultConfigDir, defaultAddrBookName) + defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName) + defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName) + defaultPrivValKeyPath = filepath.Join(defaultConfigDir, defaultPrivValKeyName) + defaultPrivValStatePath = filepath.Join(defaultDataDir, defaultPrivValStateName) + + defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName) + defaultAddrBookPath = filepath.Join(defaultConfigDir, defaultAddrBookName) ) // Config defines the top level configuration for a Tendermint node @@ -160,7 +164,10 @@ type BaseConfig struct { Genesis string `mapstructure:"genesis_file"` // Path to the JSON file containing the private key to use as a validator in the consensus protocol - PrivValidator string `mapstructure:"priv_validator_file"` + PrivValidatorKey string `mapstructure:"priv_validator_key_file"` + + // Path to the JSON file containing the last sign state of a validator + PrivValidatorState string `mapstructure:"priv_validator_state_file"` // TCP or UNIX socket address for Tendermint to listen on for // connections from an external PrivValidator process @@ -183,19 +190,20 @@ type BaseConfig struct { // DefaultBaseConfig returns a default base configuration for a Tendermint node func DefaultBaseConfig() BaseConfig { return BaseConfig{ - Genesis: defaultGenesisJSONPath, - PrivValidator: defaultPrivValPath, - NodeKey: defaultNodeKeyPath, - Moniker: defaultMoniker, - ProxyApp: "tcp://127.0.0.1:26658", - ABCI: "socket", - LogLevel: DefaultPackageLogLevels(), - LogFormat: LogFormatPlain, - ProfListenAddress: "", - FastSync: true, - FilterPeers: false, - DBBackend: "leveldb", - DBPath: "data", + Genesis: defaultGenesisJSONPath, + PrivValidatorKey: defaultPrivValKeyPath, + PrivValidatorState: defaultPrivValStatePath, + NodeKey: defaultNodeKeyPath, + Moniker: defaultMoniker, + ProxyApp: "tcp://127.0.0.1:26658", + ABCI: "socket", + LogLevel: DefaultPackageLogLevels(), + LogFormat: LogFormatPlain, + ProfListenAddress: "", + FastSync: true, + FilterPeers: false, + DBBackend: "leveldb", + DBPath: "data", } } @@ -218,9 +226,14 @@ func (cfg BaseConfig) GenesisFile() string { return rootify(cfg.Genesis, cfg.RootDir) } -// PrivValidatorFile returns the full path to the priv_validator.json file -func (cfg BaseConfig) PrivValidatorFile() string { - return rootify(cfg.PrivValidator, cfg.RootDir) +// PrivValidatorKeyFile returns the full path to the priv_validator_key.json file +func (cfg BaseConfig) PrivValidatorKeyFile() string { + return rootify(cfg.PrivValidatorKey, cfg.RootDir) +} + +// PrivValidatorFile returns the full path to the priv_validator_state.json file +func (cfg BaseConfig) PrivValidatorStateFile() string { + return rootify(cfg.PrivValidatorState, cfg.RootDir) } // NodeKeyFile returns the full path to the node_key.json file diff --git a/config/toml.go b/config/toml.go index 21e017b4..ad48e211 100644 --- a/config/toml.go +++ b/config/toml.go @@ -95,7 +95,10 @@ log_format = "{{ .BaseConfig.LogFormat }}" genesis_file = "{{ js .BaseConfig.Genesis }}" # Path to the JSON file containing the private key to use as a validator in the consensus protocol -priv_validator_file = "{{ js .BaseConfig.PrivValidator }}" +priv_validator_key_file = "{{ js .BaseConfig.PrivValidatorKey }}" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "{{ js .BaseConfig.PrivValidatorState }}" # TCP or UNIX socket address for Tendermint to listen on for # connections from an external PrivValidator process @@ -342,7 +345,8 @@ func ResetTestRoot(testName string) *Config { baseConfig := DefaultBaseConfig() configFilePath := filepath.Join(rootDir, defaultConfigFilePath) genesisFilePath := filepath.Join(rootDir, baseConfig.Genesis) - privFilePath := filepath.Join(rootDir, baseConfig.PrivValidator) + privKeyFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorKey) + privStateFilePath := filepath.Join(rootDir, baseConfig.PrivValidatorState) // Write default config file if missing. if !cmn.FileExists(configFilePath) { @@ -352,7 +356,8 @@ func ResetTestRoot(testName string) *Config { cmn.MustWriteFile(genesisFilePath, []byte(testGenesis), 0644) } // we always overwrite the priv val - cmn.MustWriteFile(privFilePath, []byte(testPrivValidator), 0644) + cmn.MustWriteFile(privKeyFilePath, []byte(testPrivValidatorKey), 0644) + cmn.MustWriteFile(privStateFilePath, []byte(testPrivValidatorState), 0644) config := TestConfig().SetRoot(rootDir) return config @@ -374,7 +379,7 @@ var testGenesis = `{ "app_hash": "" }` -var testPrivValidator = `{ +var testPrivValidatorKey = `{ "address": "A3258DCBF45DCA0DF052981870F2D1441A36D145", "pub_key": { "type": "tendermint/PubKeyEd25519", @@ -383,8 +388,11 @@ var testPrivValidator = `{ "priv_key": { "type": "tendermint/PrivKeyEd25519", "value": "EVkqJO/jIXp3rkASXfh9YnyToYXRXhBr6g9cQVxPFnQBP/5povV4HTjvsy530kybxKHwEi85iU8YL0qQhSYVoQ==" - }, - "last_height": "0", - "last_round": "0", - "last_step": 0 + } +}` + +var testPrivValidatorState = `{ + "height": "0", + "round": "0", + "step": 0 }` diff --git a/config/toml_test.go b/config/toml_test.go index a1637f67..59528db1 100644 --- a/config/toml_test.go +++ b/config/toml_test.go @@ -60,7 +60,7 @@ func TestEnsureTestRoot(t *testing.T) { // TODO: make sure the cfg returned and testconfig are the same! baseConfig := DefaultBaseConfig() - ensureFiles(t, rootDir, defaultDataDir, baseConfig.Genesis, baseConfig.PrivValidator) + ensureFiles(t, rootDir, defaultDataDir, baseConfig.Genesis, baseConfig.PrivValidatorKey, baseConfig.PrivValidatorState) } func checkConfig(configFile string) bool { diff --git a/consensus/common_test.go b/consensus/common_test.go index 46be5cbd..aa0c7551 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -281,9 +281,10 @@ func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.S } func loadPrivValidator(config *cfg.Config) *privval.FilePV { - privValidatorFile := config.PrivValidatorFile() - ensureDir(path.Dir(privValidatorFile), 0700) - privValidator := privval.LoadOrGenFilePV(privValidatorFile) + privValidatorKeyFile := config.PrivValidatorKeyFile() + ensureDir(path.Dir(privValidatorKeyFile), 0700) + privValidatorStateFile := config.PrivValidatorStateFile() + privValidator := privval.LoadOrGenFilePV(privValidatorKeyFile, privValidatorStateFile) privValidator.Reset() return privValidator } @@ -617,11 +618,16 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF if i < nValidators { privVal = privVals[i] } else { - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") if err != nil { panic(err) } - privVal = privval.GenFilePV(tempFile.Name()) + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + if err != nil { + panic(err) + } + + privVal = privval.GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) } app := appFunc() diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 7cd32c7a..71b93775 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -319,7 +319,7 @@ func testHandshakeReplay(t *testing.T, nBlocks int, mode uint) { walFile := tempWALWithData(walBody) config.Consensus.SetWalFile(walFile) - privVal := privval.LoadFilePV(config.PrivValidatorFile()) + privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) wal, err := NewWAL(walFile) require.NoError(t, err) @@ -633,7 +633,7 @@ func TestInitChainUpdateValidators(t *testing.T) { clientCreator := proxy.NewLocalClientCreator(app) config := ResetConfig("proxy_test_") - privVal := privval.LoadFilePV(config.PrivValidatorFile()) + privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) stateDB, state, store := stateAndStore(config, privVal.GetPubKey(), 0x0) oldValAddr := state.Validators.Validators[0].Address diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index 5ff597a5..83861d3e 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -40,8 +40,9 @@ func WALGenerateNBlocks(wr io.Writer, numBlocks int) (err error) { // COPY PASTE FROM node.go WITH A FEW MODIFICATIONS // NOTE: we can't import node package because of circular dependency. // NOTE: we don't do handshake so need to set state.Version.Consensus.App directly. - privValidatorFile := config.PrivValidatorFile() - privValidator := privval.LoadOrGenFilePV(privValidatorFile) + privValidatorKeyFile := config.PrivValidatorKeyFile() + privValidatorStateFile := config.PrivValidatorStateFile() + privValidator := privval.LoadOrGenFilePV(privValidatorKeyFile, privValidatorStateFile) genDoc, err := types.GenesisDocFromFile(config.GenesisFile()) if err != nil { return errors.Wrap(err, "failed to read genesis file") diff --git a/node/node.go b/node/node.go index 8e41dfd1..f03e5bab 100644 --- a/node/node.go +++ b/node/node.go @@ -87,7 +87,7 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { return nil, err } return NewNode(config, - privval.LoadOrGenFilePV(config.PrivValidatorFile()), + privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()), nodeKey, proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), DefaultGenesisDocProviderFunc(config), diff --git a/privval/priv_validator.go b/privval/priv_validator.go index ba777e1f..7d53c568 100644 --- a/privval/priv_validator.go +++ b/privval/priv_validator.go @@ -37,21 +37,31 @@ func voteToStep(vote *types.Vote) int8 { // FilePV implements PrivValidator using data persisted to disk // to prevent double signing. -// NOTE: the directory containing the pv.filePath must already exist. +// 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 { - Address types.Address `json:"address"` - PubKey crypto.PubKey `json:"pub_key"` - LastHeight int64 `json:"last_height"` - LastRound int `json:"last_round"` - LastStep int8 `json:"last_step"` - LastSignature []byte `json:"last_signature,omitempty"` - LastSignBytes cmn.HexBytes `json:"last_signbytes,omitempty"` - PrivKey crypto.PrivKey `json:"priv_key"` + Key FilePVKey + LastSignState FilePVLastSignState +} + +// 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 +} + +// FilePVState stores the mutable part of PrivValidator +type FilePVLastSignState struct { + Height int64 `json:"height"` + Round int `json:"round"` + Step int8 `json:"step"` + Signature []byte `json:"signature,omitempty"` + SignBytes cmn.HexBytes `json:"signbytes,omitempty"` - // For persistence. - // Overloaded for testing. filePath string mtx sync.Mutex } @@ -59,58 +69,82 @@ type FilePV struct { // GetAddress returns the address of the validator. // Implements PrivValidator. func (pv *FilePV) GetAddress() types.Address { - return pv.Address + return pv.Key.Address } // GetPubKey returns the public key of the validator. // Implements PrivValidator. func (pv *FilePV) GetPubKey() crypto.PubKey { - return pv.PubKey + return pv.Key.PubKey } // GenFilePV generates a new validator with randomly generated private key -// and sets the filePath, but does not call Save(). -func GenFilePV(filePath string) *FilePV { +// and sets the filePaths, but does not call Save(). +func GenFilePV(keyFilePath string, stateFilePath string) *FilePV { privKey := ed25519.GenPrivKey() + return &FilePV{ - Address: privKey.PubKey().Address(), - PubKey: privKey.PubKey(), - PrivKey: privKey, - LastStep: stepNone, - filePath: filePath, + Key: FilePVKey{ + Address: privKey.PubKey().Address(), + PubKey: privKey.PubKey(), + PrivKey: privKey, + filePath: keyFilePath, + }, + LastSignState: FilePVLastSignState{ + Step: stepNone, + filePath: stateFilePath, + }, } } -// LoadFilePV loads a FilePV from the filePath. The FilePV handles double -// signing prevention by persisting data to the filePath. If the filePath does +// LoadFilePV loads a FilePV from the filePaths. The FilePV handles double +// signing prevention by persisting data to the stateFilePath. If the filePaths does // not exist, the FilePV must be created manually and saved. -func LoadFilePV(filePath string) *FilePV { - pvJSONBytes, err := ioutil.ReadFile(filePath) +func LoadFilePV(keyFilePath string, stateFilePath string) *FilePV { + keyJSONBytes, err := ioutil.ReadFile(keyFilePath) if err != nil { cmn.Exit(err.Error()) } - pv := &FilePV{} - err = cdc.UnmarshalJSON(pvJSONBytes, &pv) + pvKey := FilePVKey{} + err = cdc.UnmarshalJSON(keyJSONBytes, &pvKey) if err != nil { - cmn.Exit(fmt.Sprintf("Error reading PrivValidator from %v: %v\n", filePath, err)) + cmn.Exit(fmt.Sprintf("Error reading PrivValidator key from %v: %v\n", keyFilePath, err)) } // overwrite pubkey and address for convenience - pv.PubKey = pv.PrivKey.PubKey() - pv.Address = pv.PubKey.Address() + pvKey.PubKey = pvKey.PrivKey.PubKey() + pvKey.Address = pvKey.PubKey.Address() + pvKey.filePath = keyFilePath + + stateJSONBytes, err := ioutil.ReadFile(stateFilePath) + if err != nil { + cmn.Exit(err.Error()) + } + pvState := FilePVLastSignState{} + err = cdc.UnmarshalJSON(stateJSONBytes, &pvState) + if err != nil { + cmn.Exit(fmt.Sprintf("Error reading PrivValidator state from %v: %v\n", stateFilePath, err)) + } + + pvState.filePath = stateFilePath + + pv := &FilePV{} + + pv.Key = pvKey + pv.LastSignState = pvState - pv.filePath = filePath return pv } -// LoadOrGenFilePV loads a FilePV from the given filePath -// or else generates a new one and saves it to the filePath. -func LoadOrGenFilePV(filePath string) *FilePV { +// LoadOrGenFilePV loads a FilePV from the given filePaths +// or else generates a new one and saves it to the filePaths. +func LoadOrGenFilePV(keyFilePath string, stateFilePath string) *FilePV { var pv *FilePV - if cmn.FileExists(filePath) { - pv = LoadFilePV(filePath) + if cmn.FileExists(keyFilePath) { + println(keyFilePath) + pv = LoadFilePV(keyFilePath, stateFilePath) } else { - pv = GenFilePV(filePath) + pv = GenFilePV(keyFilePath, stateFilePath) pv.Save() } return pv @@ -118,17 +152,36 @@ func LoadOrGenFilePV(filePath string) *FilePV { // Save persists the FilePV to disk. func (pv *FilePV) Save() { - pv.mtx.Lock() - defer pv.mtx.Unlock() - pv.save() + pv.saveKey() + + pv.LastSignState.mtx.Lock() + defer pv.LastSignState.mtx.Unlock() + pv.saveState() } -func (pv *FilePV) save() { - outFile := pv.filePath +func (pv *FilePV) saveKey() { + outFile := pv.Key.filePath if outFile == "" { - panic("Cannot save PrivValidator: filePath not set") + panic("Cannot save PrivValidator key: filePath not set") } - jsonBytes, err := cdc.MarshalJSONIndent(pv, "", " ") + + jsonBytes, err := cdc.MarshalJSONIndent(pv.Key, "", " ") + if err != nil { + panic(err) + } + err = cmn.WriteFileAtomic(outFile, jsonBytes, 0600) + if err != nil { + panic(err) + } + +} + +func (pv *FilePV) saveState() { + outFile := pv.LastSignState.filePath + if outFile == "" { + panic("Cannot save PrivValidator state: filePath not set") + } + jsonBytes, err := cdc.MarshalJSONIndent(pv.LastSignState, "", " ") if err != nil { panic(err) } @@ -142,19 +195,19 @@ func (pv *FilePV) save() { // NOTE: Unsafe! func (pv *FilePV) Reset() { var sig []byte - pv.LastHeight = 0 - pv.LastRound = 0 - pv.LastStep = 0 - pv.LastSignature = sig - pv.LastSignBytes = nil + pv.LastSignState.Height = 0 + pv.LastSignState.Round = 0 + pv.LastSignState.Step = 0 + pv.LastSignState.Signature = sig + pv.LastSignState.SignBytes = nil pv.Save() } // SignVote signs a canonical representation of the vote, along with the // chainID. Implements PrivValidator. func (pv *FilePV) SignVote(chainID string, vote *types.Vote) error { - pv.mtx.Lock() - defer pv.mtx.Unlock() + pv.LastSignState.mtx.Lock() + defer pv.LastSignState.mtx.Unlock() if err := pv.signVote(chainID, vote); err != nil { return fmt.Errorf("Error signing vote: %v", err) } @@ -164,8 +217,8 @@ func (pv *FilePV) SignVote(chainID string, vote *types.Vote) error { // SignProposal signs a canonical representation of the proposal, along with // the chainID. Implements PrivValidator. func (pv *FilePV) SignProposal(chainID string, proposal *types.Proposal) error { - pv.mtx.Lock() - defer pv.mtx.Unlock() + pv.LastSignState.mtx.Lock() + defer pv.LastSignState.mtx.Unlock() if err := pv.signProposal(chainID, proposal); err != nil { return fmt.Errorf("Error signing proposal: %v", err) } @@ -174,21 +227,21 @@ func (pv *FilePV) SignProposal(chainID string, proposal *types.Proposal) error { // returns error if HRS regression or no LastSignBytes. returns true if HRS is unchanged func (pv *FilePV) checkHRS(height int64, round int, step int8) (bool, error) { - if pv.LastHeight > height { + if pv.LastSignState.Height > height { return false, errors.New("Height regression") } - if pv.LastHeight == height { - if pv.LastRound > round { + if pv.LastSignState.Height == height { + if pv.LastSignState.Round > round { return false, errors.New("Round regression") } - if pv.LastRound == round { - if pv.LastStep > step { + if pv.LastSignState.Round == round { + if pv.LastSignState.Step > step { return false, errors.New("Step regression") - } else if pv.LastStep == step { - if pv.LastSignBytes != nil { - if pv.LastSignature == nil { + } else if pv.LastSignState.Step == step { + if pv.LastSignState.SignBytes != nil { + if pv.LastSignState.Signature == nil { panic("pv: LastSignature is nil but LastSignBytes is not!") } return true, nil @@ -218,11 +271,11 @@ func (pv *FilePV) signVote(chainID string, vote *types.Vote) error { // If they only differ by timestamp, use last timestamp and signature // Otherwise, return error if sameHRS { - if bytes.Equal(signBytes, pv.LastSignBytes) { - vote.Signature = pv.LastSignature - } else if timestamp, ok := checkVotesOnlyDifferByTimestamp(pv.LastSignBytes, signBytes); ok { + if bytes.Equal(signBytes, pv.LastSignState.SignBytes) { + vote.Signature = pv.LastSignState.Signature + } else if timestamp, ok := checkVotesOnlyDifferByTimestamp(pv.LastSignState.SignBytes, signBytes); ok { vote.Timestamp = timestamp - vote.Signature = pv.LastSignature + vote.Signature = pv.LastSignState.Signature } else { err = fmt.Errorf("Conflicting data") } @@ -230,7 +283,7 @@ func (pv *FilePV) signVote(chainID string, vote *types.Vote) error { } // It passed the checks. Sign the vote - sig, err := pv.PrivKey.Sign(signBytes) + sig, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { return err } @@ -257,11 +310,11 @@ func (pv *FilePV) signProposal(chainID string, proposal *types.Proposal) error { // If they only differ by timestamp, use last timestamp and signature // Otherwise, return error if sameHRS { - if bytes.Equal(signBytes, pv.LastSignBytes) { - proposal.Signature = pv.LastSignature - } else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(pv.LastSignBytes, signBytes); ok { + if bytes.Equal(signBytes, pv.LastSignState.SignBytes) { + proposal.Signature = pv.LastSignState.Signature + } else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(pv.LastSignState.SignBytes, signBytes); ok { proposal.Timestamp = timestamp - proposal.Signature = pv.LastSignature + proposal.Signature = pv.LastSignState.Signature } else { err = fmt.Errorf("Conflicting data") } @@ -269,7 +322,7 @@ func (pv *FilePV) signProposal(chainID string, proposal *types.Proposal) error { } // It passed the checks. Sign the proposal - sig, err := pv.PrivKey.Sign(signBytes) + sig, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { return err } @@ -282,17 +335,17 @@ func (pv *FilePV) signProposal(chainID string, proposal *types.Proposal) error { func (pv *FilePV) saveSigned(height int64, round int, step int8, signBytes []byte, sig []byte) { - pv.LastHeight = height - pv.LastRound = round - pv.LastStep = step - pv.LastSignature = sig - pv.LastSignBytes = signBytes - pv.save() + pv.LastSignState.Height = height + pv.LastSignState.Round = round + pv.LastSignState.Step = step + pv.LastSignState.Signature = sig + pv.LastSignState.SignBytes = signBytes + pv.saveState() } // 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.LastHeight, pv.LastRound, pv.LastStep) + return fmt.Sprintf("PrivValidator{%v LH:%v, LR:%v, LS:%v}", pv.GetAddress(), pv.LastSignState.Height, pv.LastSignState.Round, pv.LastSignState.Step) } //------------------------------------- diff --git a/privval/priv_validator_test.go b/privval/priv_validator_test.go index 4f4eed97..39465692 100644 --- a/privval/priv_validator_test.go +++ b/privval/priv_validator_test.go @@ -18,36 +18,72 @@ import ( func TestGenLoadValidator(t *testing.T) { assert := assert.New(t) - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") require.Nil(t, err) - privVal := GenFilePV(tempFile.Name()) + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + require.Nil(t, err) + + privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) height := int64(100) - privVal.LastHeight = height + privVal.LastSignState.Height = height privVal.Save() addr := privVal.GetAddress() - privVal = LoadFilePV(tempFile.Name()) + privVal = LoadFilePV(tempKeyFile.Name(), tempStateFile.Name()) assert.Equal(addr, privVal.GetAddress(), "expected privval addr to be the same") - assert.Equal(height, privVal.LastHeight, "expected privval.LastHeight to have been saved") + assert.Equal(height, privVal.LastSignState.Height, "expected privval.LastHeight to have been saved") } func TestLoadOrGenValidator(t *testing.T) { assert := assert.New(t) - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") require.Nil(t, err) - tempFilePath := tempFile.Name() - if err := os.Remove(tempFilePath); err != nil { + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + require.Nil(t, err) + + tempKeyFilePath := tempKeyFile.Name() + if err := os.Remove(tempKeyFilePath); err != nil { t.Error(err) } - privVal := LoadOrGenFilePV(tempFilePath) + tempStateFilePath := tempStateFile.Name() + if err := os.Remove(tempStateFilePath); err != nil { + t.Error(err) + } + + privVal := LoadOrGenFilePV(tempKeyFilePath, tempStateFilePath) addr := privVal.GetAddress() - privVal = LoadOrGenFilePV(tempFilePath) + privVal = LoadOrGenFilePV(tempKeyFilePath, tempStateFilePath) assert.Equal(addr, privVal.GetAddress(), "expected privval addr to be the same") } -func TestUnmarshalValidator(t *testing.T) { +func TestUnmarshalValidatorState(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // create some fixed values + serialized := fmt.Sprintf(`{ + "height": "1", + "round": "1", + "step": 1 + }`) + + val := FilePVLastSignState{} + err := cdc.UnmarshalJSON([]byte(serialized), &val) + require.Nil(err, "%+v", err) + + // make sure the values match + assert.EqualValues(val.Height, 1) + assert.EqualValues(val.Round, 1) + assert.EqualValues(val.Step, 1) + + // export it and make sure it is the same + out, err := cdc.MarshalJSON(val) + require.Nil(err, "%+v", err) + assert.JSONEq(serialized, string(out)) +} + +func TestUnmarshalValidatorKey(t *testing.T) { assert, require := assert.New(t), require.New(t) // create some fixed values @@ -67,22 +103,19 @@ func TestUnmarshalValidator(t *testing.T) { "type": "tendermint/PubKeyEd25519", "value": "%s" }, - "last_height": "0", - "last_round": "0", - "last_step": 0, "priv_key": { "type": "tendermint/PrivKeyEd25519", "value": "%s" } }`, addr, pubB64, privB64) - val := FilePV{} + val := FilePVKey{} err := cdc.UnmarshalJSON([]byte(serialized), &val) require.Nil(err, "%+v", err) // make sure the values match - assert.EqualValues(addr, val.GetAddress()) - assert.EqualValues(pubKey, val.GetPubKey()) + assert.EqualValues(addr, val.Address) + assert.EqualValues(pubKey, val.PubKey) assert.EqualValues(privKey, val.PrivKey) // export it and make sure it is the same @@ -94,9 +127,12 @@ func TestUnmarshalValidator(t *testing.T) { func TestSignVote(t *testing.T) { assert := assert.New(t) - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") require.Nil(t, err) - privVal := GenFilePV(tempFile.Name()) + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + require.Nil(t, err) + + privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{}} @@ -104,7 +140,7 @@ func TestSignVote(t *testing.T) { voteType := byte(types.PrevoteType) // sign a vote for first time - vote := newVote(privVal.Address, 0, height, round, voteType, block1) + vote := newVote(privVal.Key.Address, 0, height, round, voteType, block1) err = privVal.SignVote("mychainid", vote) assert.NoError(err, "expected no error signing vote") @@ -114,10 +150,10 @@ func TestSignVote(t *testing.T) { // now try some bad votes cases := []*types.Vote{ - newVote(privVal.Address, 0, height, round-1, voteType, block1), // round regression - newVote(privVal.Address, 0, height-1, round, voteType, block1), // height regression - newVote(privVal.Address, 0, height-2, round+4, voteType, block1), // height regression and different round - newVote(privVal.Address, 0, height, round, voteType, block2), // different block + newVote(privVal.Key.Address, 0, height, round-1, voteType, block1), // round regression + newVote(privVal.Key.Address, 0, height-1, round, voteType, block1), // height regression + newVote(privVal.Key.Address, 0, height-2, round+4, voteType, block1), // height regression and different round + newVote(privVal.Key.Address, 0, height, round, voteType, block2), // different block } for _, c := range cases { @@ -136,9 +172,12 @@ func TestSignVote(t *testing.T) { func TestSignProposal(t *testing.T) { assert := assert.New(t) - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") require.Nil(t, err) - privVal := GenFilePV(tempFile.Name()) + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + require.Nil(t, err) + + privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{10, []byte{3, 2, 1}}} @@ -175,9 +214,12 @@ func TestSignProposal(t *testing.T) { } func TestDifferByTimestamp(t *testing.T) { - tempFile, err := ioutil.TempFile("", "priv_validator_") + tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") require.Nil(t, err) - privVal := GenFilePV(tempFile.Name()) + tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + require.Nil(t, err) + + privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} height, round := int64(10), 1 @@ -208,7 +250,7 @@ func TestDifferByTimestamp(t *testing.T) { { voteType := byte(types.PrevoteType) blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} - vote := newVote(privVal.Address, 0, height, round, voteType, blockID) + vote := newVote(privVal.Key.Address, 0, height, round, voteType, blockID) err := privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index e68ec149..b89c0a17 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -119,8 +119,9 @@ func NewTendermint(app abci.Application) *nm.Node { config := GetConfig() logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) logger = log.NewFilter(logger, log.AllowError()) - pvFile := config.PrivValidatorFile() - pv := privval.LoadOrGenFilePV(pvFile) + pvKeyFile := config.PrivValidatorKeyFile() + pvKeyStateFile := config.PrivValidatorStateFile() + pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) papp := proxy.NewLocalClientCreator(app) nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) if err != nil { diff --git a/scripts/wire2amino.go b/scripts/wire2amino.go index 26069b50..da23c30c 100644 --- a/scripts/wire2amino.go +++ b/scripts/wire2amino.go @@ -48,6 +48,18 @@ type PrivVal struct { PrivKey Data `json:"priv_key"` } +type PrivValKey struct { + Address cmn.HexBytes `json:"address"` + PubKey Data `json:"pub_key"` + PrivKey Data `json:"priv_key"` +} + +type PrivValState struct { + LastHeight int64 `json:"last_height"` + LastRound int `json:"last_round"` + LastStep int8 `json:"last_step"` +} + type Data struct { Type string `json:"type"` Data cmn.HexBytes `json:"data"` @@ -72,8 +84,8 @@ func convertNodeKey(cdc *amino.Codec, jsonBytes []byte) ([]byte, error) { return bz, nil } -func convertPrivVal(cdc *amino.Codec, jsonBytes []byte) ([]byte, error) { - var privVal PrivVal +func convertPrivValKey(cdc *amino.Codec, jsonBytes []byte) ([]byte, error) { + var privVal PrivValKey err := json.Unmarshal(jsonBytes, &privVal) if err != nil { return nil, err @@ -85,13 +97,30 @@ func convertPrivVal(cdc *amino.Codec, jsonBytes []byte) ([]byte, error) { var pubKey ed25519.PubKeyEd25519 copy(pubKey[:], privVal.PubKey.Data) - privValNew := privval.FilePV{ - Address: pubKey.Address(), - PubKey: pubKey, - LastHeight: privVal.LastHeight, - LastRound: privVal.LastRound, - LastStep: privVal.LastStep, - PrivKey: privKey, + privValNew := privval.FilePVKey{ + Address: pubKey.Address(), + PubKey: pubKey, + PrivKey: privKey, + } + + bz, err := cdc.MarshalJSON(privValNew) + if err != nil { + return nil, err + } + return bz, nil +} + +func convertPrivValState(cdc *amino.Codec, jsonBytes []byte) ([]byte, error) { + var privVal PrivValState + err := json.Unmarshal(jsonBytes, &privVal) + if err != nil { + return nil, err + } + + privValNew := privval.FilePVLastSignState{ + Height: privVal.LastHeight, + Round: privVal.LastRound, + Step: privVal.LastStep, } bz, err := cdc.MarshalJSON(privValNew) @@ -165,8 +194,10 @@ func main() { switch fileName { case "node_key.json": bz, err = convertNodeKey(cdc, fileBytes) - case "priv_validator.json": - bz, err = convertPrivVal(cdc, fileBytes) + case "priv_validator_key.json": + bz, err = convertPrivValKey(cdc, fileBytes) + case "priv_validator_state.json": + bz, err = convertPrivValState(cdc, fileBytes) case "genesis.json": bz, err = convertGenesis(cdc, fileBytes) default: