-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: unsafe reset all #1196
feat: unsafe reset all #1196
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
) | ||
|
||
func main() { | ||
rootCmd := newRootCmd() | ||
if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { | ||
_, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) | ||
os.Exit(1) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm against modifying global variables like os.Stdin for testing purposes. I think commands.IO works well, allows us for mock tests, and doesn't change anything in terms of security, on top of everything because this code is in a main package which cannot be imported. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retaining os.Stdin wrapped in commands.IO within the gno.land runtime presents a security vulnerability. Here are the concerns.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Edit: my review covers this pull request: #1333, where the security and flexibility issues are clearer IMO. Please look at my latest comment here: https://github.com/gnolang/gno/pull/1196/files#r1385496885, where I suggest going back to the io.Commands approach but with a new read-only interface (without os.Stdin). This would help ensure readonly commands remain so by preventing accidental os.Stdin inputs. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
) | ||
|
||
// This is for testing purposes only. | ||
// For mocking tests, we redirect os.Stdin so that we don't need to pass commands.IO, | ||
// which includes os.Stdin, to all the server commands. Exposing os.Stdin in a blockchain node is not safe. | ||
// This replaces the global variable and should not be used in concurrent tests. It's intended to simulate CLI input. | ||
// We purposely avoid using a mutex to prevent giving the wrong impression that it's suitable for parallel tests. | ||
|
||
type MockStdin struct { | ||
origStdout *os.File | ||
stdoutReader *os.File | ||
|
||
outCh chan []byte | ||
|
||
origStdin *os.File | ||
stdinWriter *os.File | ||
} | ||
|
||
func NewMockStdin(input string) (*MockStdin, error) { | ||
// Pipe for stdin. w ( stdinWriter ) -> r (stdin) | ||
stdinReader, stdinWriter, err := os.Pipe() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Pipe for stdout. w( stdout ) -> r (stdoutReader) | ||
stdoutReader, stdoutWriter, err := os.Pipe() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
origStdin := os.Stdin | ||
os.Stdin = stdinReader | ||
|
||
_, err = stdinWriter.Write([]byte(input)) | ||
if err != nil { | ||
stdinWriter.Close() | ||
os.Stdin = origStdin | ||
return nil, err | ||
} | ||
|
||
origStdout := os.Stdout | ||
os.Stdout = stdoutWriter | ||
|
||
outCh := make(chan []byte) | ||
|
||
// This goroutine reads stdout into a buffer in the background. | ||
go func() { | ||
var b bytes.Buffer | ||
if _, err := io.Copy(&b, stdoutReader); err != nil { | ||
log.Println(err) | ||
} | ||
outCh <- b.Bytes() | ||
}() | ||
|
||
return &MockStdin{ | ||
origStdout: origStdout, | ||
stdoutReader: stdoutReader, | ||
outCh: outCh, | ||
origStdin: origStdin, | ||
stdinWriter: stdinWriter, | ||
}, nil | ||
} | ||
|
||
// ReadAndRestore collects all captured stdout and returns it; it also restores | ||
// os.Stdin and os.Stdout to their original values. | ||
func (i *MockStdin) ReadAndClose() ([]byte, error) { | ||
if i.stdoutReader == nil { | ||
return nil, fmt.Errorf("ReadAndRestore from closed FakeStdio") | ||
} | ||
|
||
// Close the writer side of the faked stdout pipe. This signals to the | ||
// background goroutine that it should exit. | ||
os.Stdout.Close() | ||
out := <-i.outCh | ||
|
||
os.Stdout = i.origStdout | ||
os.Stdin = i.origStdin | ||
|
||
if i.stdoutReader != nil { | ||
i.stdoutReader.Close() | ||
i.stdoutReader = nil | ||
} | ||
|
||
if i.stdinWriter != nil { | ||
i.stdinWriter.Close() | ||
i.stdinWriter = nil | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
// Call this in a defer function to restore and close os.Stdout and os.Stdin. | ||
// This acts as a safeguard. | ||
func (i *MockStdin) Close() { | ||
os.Stdout = i.origStdout | ||
os.Stdin = i.origStdin | ||
|
||
if i.stdoutReader != nil { | ||
i.stdoutReader.Close() | ||
i.stdoutReader = nil | ||
} | ||
|
||
if i.stdinWriter != nil { | ||
i.stdinWriter.Close() | ||
i.stdinWriter = nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/gnolang/gno/tm2/pkg/bft/privval" | ||
"github.com/gnolang/gno/tm2/pkg/commands" | ||
|
||
"github.com/gnolang/gno/tm2/pkg/log" | ||
osm "github.com/gnolang/gno/tm2/pkg/os" | ||
) | ||
|
||
type resetCfg struct { | ||
baseCfg | ||
} | ||
|
||
func (rc *resetCfg) RegisterFlags(fs *flag.FlagSet) {} | ||
|
||
// XXX: this is totally unsafe. | ||
// it's only suitable for testnets. | ||
// It could result in data loss and network disrutpion while running the node and without coordination | ||
func newResetAllCmd(bc baseCfg) *commands.Command { | ||
cfg := resetCfg{ | ||
baseCfg: bc, | ||
} | ||
|
||
return commands.NewCommand( | ||
commands.Metadata{ | ||
Name: "unsafe-reset-all", | ||
ShortUsage: "unsafe-reset-all", | ||
ShortHelp: "(unsafe) Remove all the data and WAL, reset this node's validator to genesis state", | ||
}, | ||
&cfg, | ||
func(_ context.Context, args []string) error { | ||
return execResetAll(cfg, args) | ||
}, | ||
) | ||
} | ||
|
||
func execResetAll(rc resetCfg, args []string) (err error) { | ||
config := rc.tmConfig | ||
|
||
return resetAll( | ||
config.DBDir(), | ||
config.PrivValidatorKeyFile(), | ||
config.PrivValidatorStateFile(), | ||
logger, | ||
) | ||
} | ||
|
||
// resetAll removes address book files plus all data, and resets the privValdiator data. | ||
func resetAll(dbDir, privValKeyFile, privValStateFile string, logger log.Logger) error { | ||
if err := os.RemoveAll(dbDir); err == nil { | ||
logger.Info("Removed all blockchain history", "dir", dbDir) | ||
} else { | ||
logger.Error("Error removing all blockchain history", "dir", dbDir, "err", err) | ||
} | ||
|
||
if err := osm.EnsureDir(dbDir, 0o700); err != nil { | ||
logger.Error("unable to recreate dbDir", "err", err) | ||
} | ||
|
||
// recreate the dbDir since the privVal state needs to live there | ||
resetFilePV(privValKeyFile, privValStateFile, logger) | ||
return nil | ||
} | ||
|
||
// 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") | ||
gnolang := filepath.Join(dbDir, "gnolang.db") | ||
|
||
if osm.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) | ||
} | ||
Comment on lines
+73
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be a for loop over an array/slice of strings instead of duplicating the same code 4 times... also I think we can just do RemoveAll and log any error while returned by that function. 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion. |
||
} | ||
|
||
if osm.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 osm.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 osm.FileExists(gnolang) { | ||
if err := os.RemoveAll(gnolang); err == nil { | ||
logger.Info("Removed all gnolang.db", "dir", gnolang) | ||
} else { | ||
logger.Error("error removing all gnolang.db", "dir", gnolang, "err", err) | ||
} | ||
} | ||
|
||
if err := osm.EnsureDir(dbDir, 0o700); err != nil { | ||
logger.Error("unable to recreate dbDir", "err", err) | ||
} | ||
return nil | ||
} | ||
|
||
func resetFilePV(privValKeyFile, privValStateFile string, logger log.Logger) { | ||
if _, err := os.Stat(privValKeyFile); err == nil { | ||
pv := privval.LoadFilePVEmptyState(privValKeyFile, privValStateFile) | ||
pv.Reset() | ||
logger.Info( | ||
"Reset private validator file to genesis state", | ||
"keyFile", privValKeyFile, | ||
"stateFile", privValStateFile, | ||
) | ||
} else { | ||
pv := privval.GenFilePV(privValKeyFile, privValStateFile) | ||
pv.Save() | ||
logger.Info( | ||
"Generated private validator file", | ||
"keyFile", privValKeyFile, | ||
"stateFile", privValStateFile, | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package main | ||
|
||
import ( | ||
"path/filepath" | ||
"testing" | ||
|
||
bft "github.com/gnolang/gno/tm2/pkg/bft/types" | ||
tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time" | ||
"github.com/stretchr/testify/require" | ||
|
||
cfg "github.com/gnolang/gno/tm2/pkg/bft/config" | ||
"github.com/gnolang/gno/tm2/pkg/bft/privval" | ||
"github.com/gnolang/gno/tm2/pkg/p2p" | ||
) | ||
|
||
func TestResetAll(t *testing.T) { | ||
config := cfg.TestConfig() | ||
dir := t.TempDir() | ||
config.SetRootDir(dir) | ||
config.EnsureDirs() | ||
|
||
require.NoError(t, initFilesWithConfig(config)) | ||
pv := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) | ||
pv.LastSignState.Height = 10 | ||
pv.Save() | ||
|
||
require.NoError(t, resetAll(config.DBDir(), config.PrivValidatorKeyFile(), | ||
config.PrivValidatorStateFile(), logger)) | ||
|
||
require.DirExists(t, config.DBDir()) | ||
require.NoFileExists(t, filepath.Join(config.DBDir(), "block.db")) | ||
require.NoFileExists(t, filepath.Join(config.DBDir(), "state.db")) | ||
require.NoFileExists(t, filepath.Join(config.DBDir(), "gnolang.db")) | ||
require.FileExists(t, config.PrivValidatorStateFile()) | ||
require.FileExists(t, config.GenesisFile()) | ||
pv = privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) | ||
require.Equal(t, int64(0), pv.LastSignState.Height) | ||
} | ||
|
||
func initFilesWithConfig(config *cfg.Config) error { | ||
// private validator | ||
privValKeyFile := config.PrivValidatorKeyFile() | ||
privValStateFile := config.PrivValidatorStateFile() | ||
var pv *privval.FilePV | ||
pv = privval.GenFilePV(privValKeyFile, privValStateFile) | ||
pv.Save() | ||
nodeKeyFile := config.NodeKeyFile() | ||
if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil { | ||
return err | ||
} | ||
|
||
genFile := config.GenesisFile() | ||
genDoc := bft.GenesisDoc{ | ||
ChainID: "test-chain-%v", | ||
GenesisTime: tmtime.Now(), | ||
ConsensusParams: bft.DefaultConsensusParams(), | ||
} | ||
key := pv.GetPubKey() | ||
genDoc.Validators = []bft.GenesisValidator{{ | ||
Address: key.Address(), | ||
PubKey: key, | ||
Power: 10, | ||
}} | ||
if err := genDoc.SaveAs(genFile); err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referring to #1201, to focus on a production-ready gnoland binary and maintain separate helper tools, we could simply add a contribs/gnoland-resetall, or a contribs/gnolandtools with an 'unsafe-reset-all' subcommand.