diff --git a/cmd/ostracon/commands/reset_priv_validator.go b/cmd/ostracon/commands/reset_priv_validator.go index c7d9c3eb1..12ee3d96b 100644 --- a/cmd/ostracon/commands/reset_priv_validator.go +++ b/cmd/ostracon/commands/reset_priv_validator.go @@ -2,6 +2,7 @@ package commands import ( "os" + "path/filepath" "github.com/spf13/cobra" @@ -16,12 +17,22 @@ var ResetAllCmd = &cobra.Command{ Use: "unsafe-reset-all", Aliases: []string{"unsafe_reset_all"}, Short: "(unsafe) Remove all the data and WAL, reset this node's validator to genesis state", - Run: resetAll, + RunE: resetAllCmd, PreRun: deprecateSnakeCase, } var keepAddrBook bool +// ResetStateCmd removes the database of the specified Tendermint core instance. +var ResetStateCmd = &cobra.Command{ + Use: "reset-state", + Short: "Remove all the data and WAL", + RunE: func(cmd *cobra.Command, args []string) error { + return resetState(config.DBDir(), logger) + }, + PreRun: deprecateSnakeCase, +} + func init() { ResetAllCmd.Flags().BoolVar(&keepAddrBook, "keep-addr-book", false, "keep the address book intact") ResetAllCmd.Flags().String("priv_key_type", config.PrivKeyType, @@ -41,12 +52,9 @@ var ResetPrivValidatorCmd = &cobra.Command{ // XXX: this is totally unsafe. // it's only suitable for testnets. -func resetAll(cmd *cobra.Command, args []string) { - err := ResetAll(config.DBDir(), config.P2P.AddrBookFile(), config.PrivValidatorKeyFile(), +func resetAllCmd(cmd *cobra.Command, args []string) error { + return resetAll(config.DBDir(), config.P2P.AddrBookFile(), config.PrivValidatorKeyFile(), config.PrivValidatorStateFile(), config.PrivValidatorKeyType(), logger) - if err != nil { - panic(err) - } } // XXX: this is totally unsafe. @@ -59,9 +67,8 @@ func resetPrivValidator(cmd *cobra.Command, args []string) { } } -// ResetAll removes address book files plus all data, and resets the privValdiator data. -// Exported so other CLI tools can use it. -func ResetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile, privKeyType string, logger log.Logger) error { +// resetAll removes address book files plus all data, and resets the privValdiator data. +func resetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile, privKeyType string, logger log.Logger) error { if keepAddrBook { logger.Info("The address book remains intact") } else { @@ -72,6 +79,7 @@ func ResetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile, privKeyType } else { logger.Error("Error removing all blockchain history", "dir", dbDir, "err", err) } + // recreate the dbDir since the privVal state needs to live there if err := tmos.EnsureDir(dbDir, 0700); err != nil { logger.Error("unable to recreate dbDir", "err", err) @@ -79,6 +87,68 @@ func ResetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile, privKeyType return resetFilePV(privValKeyFile, privValStateFile, privKeyType, logger) } +// resetState removes address book files plus all databases. +func resetState(dbDir string, logger log.Logger) error { + blockdb := filepath.Join(dbDir, "blockstore.db") + state := filepath.Join(dbDir, "state.db") + wal := filepath.Join(dbDir, "cs.wal") + evidence := filepath.Join(dbDir, "evidence.db") + txIndex := filepath.Join(dbDir, "tx_index.db") + peerstore := filepath.Join(dbDir, "peerstore.db") + + if tmos.FileExists(blockdb) { + if err := os.RemoveAll(blockdb); err == nil { + logger.Info("Removed all blockstore.db", "dir", blockdb) + } else { + logger.Error("error removing all blockstore.db", "dir", blockdb, "err", err) + } + } + + if tmos.FileExists(state) { + if err := os.RemoveAll(state); err == nil { + logger.Info("Removed all state.db", "dir", state) + } else { + logger.Error("error removing all state.db", "dir", state, "err", err) + } + } + + if tmos.FileExists(wal) { + if err := os.RemoveAll(wal); err == nil { + logger.Info("Removed all cs.wal", "dir", wal) + } else { + logger.Error("error removing all cs.wal", "dir", wal, "err", err) + } + } + + if tmos.FileExists(evidence) { + if err := os.RemoveAll(evidence); err == nil { + logger.Info("Removed all evidence.db", "dir", evidence) + } else { + logger.Error("error removing all evidence.db", "dir", evidence, "err", err) + } + } + + if tmos.FileExists(txIndex) { + if err := os.RemoveAll(txIndex); err == nil { + logger.Info("Removed tx_index.db", "dir", txIndex) + } else { + logger.Error("error removing tx_index.db", "dir", txIndex, "err", err) + } + } + + if tmos.FileExists(peerstore) { + if err := os.RemoveAll(peerstore); err == nil { + logger.Info("Removed peerstore.db", "dir", peerstore) + } else { + logger.Error("error removing peerstore.db", "dir", peerstore, "err", err) + } + } + if err := tmos.EnsureDir(dbDir, 0700); err != nil { + logger.Error("unable to recreate dbDir", "err", err) + } + return nil +} + func resetFilePV(privValKeyFile, privValStateFile, privKeyType string, logger log.Logger) error { if _, err := os.Stat(privValKeyFile); err == nil { pv := privval.LoadFilePVEmptyState(privValKeyFile, privValStateFile) @@ -90,7 +160,9 @@ func resetFilePV(privValKeyFile, privValStateFile, privKeyType string, logger lo if err != nil { return err } - pv.Save() + if pv != nil { + pv.Save() + } logger.Info("Generated private validator file", "keyFile", privValKeyFile, "stateFile", privValStateFile) } diff --git a/cmd/ostracon/commands/reset_priv_validator_test.go b/cmd/ostracon/commands/reset_priv_validator_test.go new file mode 100644 index 000000000..84694d4c2 --- /dev/null +++ b/cmd/ostracon/commands/reset_priv_validator_test.go @@ -0,0 +1,33 @@ +package commands + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func setupResetCmd(t *testing.T) { + clearConfig(defaultRoot) + config.SetRoot(defaultRoot) + require.NoError(t, os.MkdirAll(filepath.Dir(config.PrivValidatorKeyFile()), 0755)) + require.NoError(t, os.MkdirAll(filepath.Dir(config.PrivValidatorStateFile()), 0755)) +} + +func TestResetAllCmd(t *testing.T) { + setupResetCmd(t) + err := ResetAllCmd.RunE(ResetAllCmd, nil) + require.NoError(t, err) +} + +func TestResetStateCmd(t *testing.T) { + setupResetCmd(t) + err := ResetStateCmd.RunE(ResetStateCmd, nil) + require.NoError(t, err) +} + +func TestResetPrivValidatorCmd(t *testing.T) { + setupResetCmd(t) + ResetPrivValidatorCmd.Run(ResetPrivValidatorCmd, nil) +} diff --git a/cmd/ostracon/main.go b/cmd/ostracon/main.go index ab8f5c538..b5540b8c6 100644 --- a/cmd/ostracon/main.go +++ b/cmd/ostracon/main.go @@ -21,6 +21,7 @@ func main() { cmd.ReplayConsoleCmd, cmd.ResetAllCmd, cmd.ResetPrivValidatorCmd, + cmd.ResetStateCmd, cmd.ShowValidatorCmd, cmd.TestnetFilesCmd, cmd.ShowNodeIDCmd, diff --git a/consensus/reactor.go b/consensus/reactor.go index 5f3d2de8b..fe254883b 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -46,6 +46,7 @@ type Reactor struct { mtx tmsync.RWMutex waitSync bool eventBus *types.EventBus + rs *cstypes.RoundState Metrics *Metrics } @@ -58,6 +59,7 @@ func NewReactor(consensusState *State, waitSync bool, async bool, recvBufSize in conR := &Reactor{ conS: consensusState, waitSync: waitSync, + rs: consensusState.GetRoundState(), Metrics: NopMetrics(), } conR.BaseReactor = *p2p.NewBaseReactor("Consensus", conR, async, recvBufSize) @@ -84,6 +86,7 @@ func (conR *Reactor) OnStart() error { go conR.peerStatsRoutine() conR.subscribeToBroadcastEvents() + go conR.updateRoundStateRoutine() if !conR.WaitSync() { err := conR.conS.Start() @@ -488,11 +491,31 @@ func makeRoundStepMessage(rs *cstypes.RoundState) (nrsMsg *NewRoundStepMessage) } func (conR *Reactor) sendNewRoundStepMessage(peer p2p.Peer) { - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() nrsMsg := makeRoundStepMessage(rs) peer.Send(StateChannel, MustEncode(nrsMsg)) } +func (conR *Reactor) updateRoundStateRoutine() { + t := time.NewTicker(100 * time.Microsecond) + defer t.Stop() + for range t.C { + if !conR.IsRunning() { + return + } + rs := conR.conS.GetRoundState() + conR.mtx.Lock() + conR.rs = rs + conR.mtx.Unlock() + } +} + +func (conR *Reactor) getRoundState() *cstypes.RoundState { + conR.mtx.RLock() + defer conR.mtx.RUnlock() + return conR.rs +} + func (conR *Reactor) gossipDataRoutine(peer p2p.Peer, ps *PeerState) { logger := conR.Logger.With("peer", peer) @@ -502,7 +525,7 @@ OUTER_LOOP: if !peer.IsRunning() || !conR.IsRunning() { return } - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() prs := ps.GetRoundState() // Send proposal Block parts? @@ -645,7 +668,7 @@ OUTER_LOOP: if !peer.IsRunning() || !conR.IsRunning() { return } - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() prs := ps.GetRoundState() switch sleeping { @@ -779,7 +802,7 @@ OUTER_LOOP: // Maybe send Height/Round/Prevotes { - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() prs := ps.GetRoundState() if rs.Height == prs.Height { if maj23, ok := rs.Votes.Prevotes(prs.Round).TwoThirdsMajority(); ok { @@ -796,7 +819,7 @@ OUTER_LOOP: // Maybe send Height/Round/Precommits { - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() prs := ps.GetRoundState() if rs.Height == prs.Height { if maj23, ok := rs.Votes.Precommits(prs.Round).TwoThirdsMajority(); ok { @@ -813,7 +836,7 @@ OUTER_LOOP: // Maybe send Height/Round/ProposalPOL { - rs := conR.conS.GetRoundState() + rs := conR.getRoundState() prs := ps.GetRoundState() if rs.Height == prs.Height && prs.ProposalPOLRound >= 0 { if maj23, ok := rs.Votes.Prevotes(prs.ProposalPOLRound).TwoThirdsMajority(); ok { diff --git a/consensus/state.go b/consensus/state.go index 0fab98e7b..16bba66b6 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -357,6 +357,15 @@ func (cs *State) OnStart() error { } } + // we need the timeoutRoutine for replay so + // we don't block on the tick chan. + // NOTE: we will get a build up of garbage go routines + // firing on the tockChan until the receiveRoutine is started + // to deal with them (by that point, at most one will be valid) + if err := cs.timeoutTicker.Start(); err != nil { + return err + } + // We may have lost some votes if the process crashed reload from consensus // log to catchup. if cs.doWALCatchup { @@ -413,15 +422,6 @@ func (cs *State) OnStart() error { return err } - // we need the timeoutRoutine for replay so - // we don't block on the tick chan. - // NOTE: we will get a build up of garbage go routines - // firing on the tockChan until the receiveRoutine is started - // to deal with them (by that point, at most one will be valid) - if err := cs.timeoutTicker.Start(); err != nil { - return err - } - // Double Signing Risk Reduction if err := cs.checkDoubleSigningRisk(cs.Height); err != nil { return err @@ -852,7 +852,6 @@ func (cs *State) receiveRoutine(maxSteps int) { func (cs *State) handleMsg(mi msgInfo) { cs.mtx.Lock() defer cs.mtx.Unlock() - var ( added bool err error @@ -869,6 +868,24 @@ func (cs *State) handleMsg(mi msgInfo) { case *BlockPartMessage: // if the proposal is complete, we'll enterPrevote or tryFinalizeCommit added, err = cs.addProposalBlockPart(msg, peerID) + + // We unlock here to yield to any routines that need to read the the RoundState. + // Previously, this code held the lock from the point at which the final block + // part was recieved until the block executed against the application. + // This prevented the reactor from being able to retrieve the most updated + // version of the RoundState. The reactor needs the updated RoundState to + // gossip the now completed block. + // + // This code can be further improved by either always operating on a copy + // of RoundState and only locking when switching out State's copy of + // RoundState with the updated copy or by emitting RoundState events in + // more places for routines depending on it to listen for. + cs.mtx.Unlock() + + cs.mtx.Lock() + if added && cs.ProposalBlockParts.IsComplete() { + cs.handleCompleteProposal(msg.Height) + } if added { cs.statsMsgQueue <- mi } @@ -2006,44 +2023,43 @@ func (cs *State) addProposalBlockPart(msg *BlockPartMessage, peerID p2p.ID) (add if err := cs.eventBus.PublishEventCompleteProposal(cs.CompleteProposalEvent()); err != nil { cs.Logger.Error("failed publishing event complete proposal", "err", err) } + } + return added, nil +} - // Update Valid* if we can. - prevotes := cs.Votes.Prevotes(cs.Round) - blockID, hasTwoThirds := prevotes.TwoThirdsMajority() - if hasTwoThirds && !blockID.IsZero() && (cs.ValidRound < cs.Round) { - if cs.ProposalBlock.HashesTo(blockID.Hash) { - cs.Logger.Debug( - "updating valid block to new proposal block", - "valid_round", cs.Round, - "valid_block_hash", cs.ProposalBlock.Hash(), - ) +func (cs *State) handleCompleteProposal(blockHeight int64) { + // Update Valid* if we can. + prevotes := cs.Votes.Prevotes(cs.Round) + blockID, hasTwoThirds := prevotes.TwoThirdsMajority() + if hasTwoThirds && !blockID.IsZero() && (cs.ValidRound < cs.Round) { + if cs.ProposalBlock.HashesTo(blockID.Hash) { + cs.Logger.Debug( + "updating valid block to new proposal block", + "valid_round", cs.Round, + "valid_block_hash", cs.ProposalBlock.Hash(), + ) - cs.ValidRound = cs.Round - cs.ValidBlock = cs.ProposalBlock - cs.ValidBlockParts = cs.ProposalBlockParts - } - // TODO: In case there is +2/3 majority in Prevotes set for some - // block and cs.ProposalBlock contains different block, either - // proposer is faulty or voting power of faulty processes is more - // than 1/3. We should trigger in the future accountability - // procedure at this point. + cs.ValidRound = cs.Round + cs.ValidBlock = cs.ProposalBlock + cs.ValidBlockParts = cs.ProposalBlockParts } - - if cs.Step <= cstypes.RoundStepPropose && cs.isProposalComplete() { - // Move onto the next step - cs.enterPrevote(height, cs.Round) - if hasTwoThirds { // this is optimisation as this will be triggered when prevote is added - cs.enterPrecommit(height, cs.Round) - } - } else if cs.Step == cstypes.RoundStepCommit { - // If we're waiting on the proposal block... - cs.tryFinalizeCommit(height) + // TODO: In case there is +2/3 majority in Prevotes set for some + // block and cs.ProposalBlock contains different block, either + // proposer is faulty or voting power of faulty processes is more + // than 1/3. We should trigger in the future accountability + // procedure at this point. + } + + if cs.Step <= cstypes.RoundStepPropose && cs.isProposalComplete() { + // Move onto the next step + cs.enterPrevote(blockHeight, cs.Round) + if hasTwoThirds { // this is optimisation as this will be triggered when prevote is added + cs.enterPrecommit(blockHeight, cs.Round) } - - return added, nil + } else if cs.Step == cstypes.RoundStepCommit { + // If we're waiting on the proposal block... + cs.tryFinalizeCommit(blockHeight) } - - return added, nil } // Attempt to add the vote. if its a duplicate signature, dupeout the validator diff --git a/crypto/secp256k1/secp256k1.go b/crypto/secp256k1/secp256k1.go index fc7bac87a..2d0f0b271 100644 --- a/crypto/secp256k1/secp256k1.go +++ b/crypto/secp256k1/secp256k1.go @@ -127,6 +127,26 @@ func GenPrivKeySecp256k1(secret []byte) PrivKey { return PrivKey(privKey32) } +// used to reject malleable signatures +// see: +// - https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/signature_nocgo.go#L90-L93 +// - https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/crypto.go#L39 +var secp256k1halfN = new(big.Int).Rsh(secp256k1.S256().N, 1) + +// Sign creates an ECDSA signature on curve Secp256k1, using SHA256 on the msg. +// The returned signature will be of the form R || S (in lower-S form). +func (privKey PrivKey) Sign(msg []byte) ([]byte, error) { + priv, _ := secp256k1.PrivKeyFromBytes(secp256k1.S256(), privKey) + + sig, err := priv.Sign(crypto.Sha256(msg)) + if err != nil { + return nil, err + } + + sigBytes := serializeSig(sig) + return sigBytes, nil +} + //------------------------------------- var _ crypto.PubKey = PubKey{} @@ -181,3 +201,47 @@ func (pubKey PubKey) Equals(other crypto.PubKey) bool { func (pubKey PubKey) Type() string { return KeyType } + +// VerifySignature verifies a signature of the form R || S. +// It rejects signatures which are not in lower-S form. +func (pubKey PubKey) VerifySignature(msg []byte, sigStr []byte) bool { + if len(sigStr) != 64 { + return false + } + + pub, err := secp256k1.ParsePubKey(pubKey, secp256k1.S256()) + if err != nil { + return false + } + + // parse the signature: + signature := signatureFromBytes(sigStr) + // Reject malleable signatures. libsecp256k1 does this check but btcec doesn't. + // see: https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/signature_nocgo.go#L90-L93 + if signature.S.Cmp(secp256k1halfN) > 0 { + return false + } + + return signature.Verify(crypto.Sha256(msg), pub) +} + +// Read Signature struct from R || S. Caller needs to ensure +// that len(sigStr) == 64. +func signatureFromBytes(sigStr []byte) *secp256k1.Signature { + return &secp256k1.Signature{ + R: new(big.Int).SetBytes(sigStr[:32]), + S: new(big.Int).SetBytes(sigStr[32:64]), + } +} + +// Serialize signature to R || S. +// R, S are padded to 32 bytes respectively. +func serializeSig(sig *secp256k1.Signature) []byte { + rBytes := sig.R.Bytes() + sBytes := sig.S.Bytes() + sigBytes := make([]byte, 64) + // 0 pad the byte arrays from the left if they aren't big enough. + copy(sigBytes[32-len(rBytes):32], rBytes) + copy(sigBytes[64-len(sBytes):64], sBytes) + return sigBytes +} diff --git a/crypto/secp256k1/secp256k1_nocgo.go b/crypto/secp256k1/secp256k1_nocgo.go deleted file mode 100644 index 92c0fa3f4..000000000 --- a/crypto/secp256k1/secp256k1_nocgo.go +++ /dev/null @@ -1,76 +0,0 @@ -//go:build !libsecp256k1 -// +build !libsecp256k1 - -package secp256k1 - -import ( - "math/big" - - secp256k1 "github.com/btcsuite/btcd/btcec" - - "github.com/line/ostracon/crypto" -) - -// used to reject malleable signatures -// see: -// - https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/signature_nocgo.go#L90-L93 -// - https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/crypto.go#L39 -var secp256k1halfN = new(big.Int).Rsh(secp256k1.S256().N, 1) - -// Sign creates an ECDSA signature on curve Secp256k1, using SHA256 on the msg. -// The returned signature will be of the form R || S (in lower-S form). -func (privKey PrivKey) Sign(msg []byte) ([]byte, error) { - priv, _ := secp256k1.PrivKeyFromBytes(secp256k1.S256(), privKey) - - sig, err := priv.Sign(crypto.Sha256(msg)) - if err != nil { - return nil, err - } - - sigBytes := serializeSig(sig) - return sigBytes, nil -} - -// VerifySignature verifies a signature of the form R || S. -// It rejects signatures which are not in lower-S form. -func (pubKey PubKey) VerifySignature(msg []byte, sigStr []byte) bool { - if len(sigStr) != 64 { - return false - } - - pub, err := secp256k1.ParsePubKey(pubKey, secp256k1.S256()) - if err != nil { - return false - } - - // parse the signature: - signature := signatureFromBytes(sigStr) - // Reject malleable signatures. libsecp256k1 does this check but btcec doesn't. - // see: https://github.com/ethereum/go-ethereum/blob/f9401ae011ddf7f8d2d95020b7446c17f8d98dc1/crypto/signature_nocgo.go#L90-L93 - if signature.S.Cmp(secp256k1halfN) > 0 { - return false - } - - return signature.Verify(crypto.Sha256(msg), pub) -} - -// Read Signature struct from R || S. Caller needs to ensure -// that len(sigStr) == 64. -func signatureFromBytes(sigStr []byte) *secp256k1.Signature { - return &secp256k1.Signature{ - R: new(big.Int).SetBytes(sigStr[:32]), - S: new(big.Int).SetBytes(sigStr[32:64]), - } -} - -// Serialize signature to R || S. -// R, S are padded to 32 bytes respectively. -func serializeSig(sig *secp256k1.Signature) []byte { - rBytes := sig.R.Bytes() - sBytes := sig.S.Bytes() - sigBytes := make([]byte, 64) - // 0 pad the byte arrays from the left if they aren't big enough. - copy(sigBytes[32-len(rBytes):32], rBytes) - copy(sigBytes[64-len(sBytes):64], sBytes) - return sigBytes -} diff --git a/crypto/xchacha20poly1305/xchachapoly_test.go b/crypto/xchacha20poly1305/xchachapoly_test.go index b17b1c376..6844f7410 100644 --- a/crypto/xchacha20poly1305/xchachapoly_test.go +++ b/crypto/xchacha20poly1305/xchachapoly_test.go @@ -25,19 +25,19 @@ func TestRandom(t *testing.T) { plaintext := make([]byte, pl) _, err := cr.Read(key[:]) if err != nil { - t.Errorf("error on read: %w", err) + t.Errorf("error on read: %v", err) } _, err = cr.Read(nonce[:]) if err != nil { - t.Errorf("error on read: %w", err) + t.Errorf("error on read: %v", err) } _, err = cr.Read(ad) if err != nil { - t.Errorf("error on read: %w", err) + t.Errorf("error on read: %v", err) } _, err = cr.Read(plaintext) if err != nil { - t.Errorf("error on read: %w", err) + t.Errorf("error on read: %v", err) } aead, err := New(key[:])