diff --git a/gno.land/cmd/gnoland/README.md b/gno.land/cmd/gnoland/README.md index 5132cd7f52b..6d457caf350 100644 --- a/gno.land/cmd/gnoland/README.md +++ b/gno.land/cmd/gnoland/README.md @@ -6,8 +6,88 @@ $> cd ./gno/gno.land $> make install.gnoland -## Run `gnoland` full node +## Option 1: Run `gnoland` full node for local development $> gnoland start Afterward, you can interact with [`gnokey`](../gnokey) or launch a [`gnoweb`](../gnoweb) interface. + + +## Option 2: Run a node and sync with a Proof of Authority (POA) network + + - gnoland init + + - Download genesis.json from a trusted source and save it to the root-dir/config directory. + + - Get the peer's node id from the node you trust. + + - Start the node + + $> gnoland start --persistent "node_id@peer_ip_address:port" or add the persistent_peers value in the ./testdir/config/config.toml + +## Option 3: Run a node as a Proof of Authority validator starting from genesis state + +- Initialize the config and key files. + + $> gnoland init + +- Return the node info; we will need it to add to validator info in the genesis.json + + $> gnoland node + + Address: "g14t47gv3v2z3pc23g3zr39mnc99w2cplp0jhqvv" + Pubkey: "E5IFULgXFdS49ILgvPmO3/8chuSWfbqw3zYXaNEP+60=" + +- Download genesis.json from a trusted source and save it to the root-dir/config directory. + +- Add your validator to the genesis file. + + $> genesis validator add \ + --address g14t47gv3v2z3pc23g3zr39mnc99w2cplp0jhqvv \ + --pub-key E5IFULgXFdS49ILgvPmO3/8chuSWfbqw3zYXaNEP+60= \ + --power 10 \ + --name testvalidator2 + +- Share the genesis with all trusted validators. + +- Get the peer's node id from the archive node you trust. + +- Start the node + + $> gnoland start --persistent "node_id@peer_ip_address:port" + + or add the persistent_peers value in the ./testdir/config/config.toml + + +## Option 4: Run as an archive node starting from genesis state + +It's recommended to have at least two POA validator nodes running as archive nodes to bootstrap the network. + +Complete the steps in Option 4 and replace the last two steps with + +- Retrive node id and give it trusted peers. + + $> gnoland node + +- Start the node + + $> gnoland start --prune "nothing" + + +## Reset `gnoland` node back to genesis state. It's suitable for running test node + + $> gnoland unsafe-reset-all + +It removes the database and reset validator state back to genesis state but leaves the genesis.json and config.toml files unchanged. + +The `unsafe-reset-all` command is labeled "unsafe" because: + +1. It irreversibly deletes all node data, risking data loss. +2. It may lead to double signing or chain forks in production. +3. It resets the `priv_validator_state.json`, and can cause network disruption if uncoordinated. + +## Reset `gnoland` node history back to genesis state. + + It removes the datastore and keeps the validator state unchanged. It reduces the risk of double signing and chain fork when we sync history state from the genesis. The validator will not sign a block until the node has synced, passing the state where the validator stopped signing. + + $> gnoland reset-state diff --git a/gno.land/cmd/gnoland/init.go b/gno.land/cmd/gnoland/init.go new file mode 100644 index 00000000000..5f92d954b8e --- /dev/null +++ b/gno.land/cmd/gnoland/init.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + tmos "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +// Display a node's persistent peer ID to the standard output. +func newInitCmd(bc baseCfg) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "init", + ShortUsage: "init", + ShortHelp: "initialize gnoland node", + }, + nil, + func(_ context.Context, args []string) error { + return execInit(bc) + }, + ) + return cmd +} + +func execInit(bc baseCfg) error { + config := bc.tmConfig + // private validator + privValKeyFile := config.PrivValidatorKeyFile() + privValStateFile := config.PrivValidatorStateFile() + var pv *privval.FilePV + if tmos.FileExists(privValKeyFile) { + logger.Info("Found private validator", "keyFile", privValKeyFile, + "stateFile", privValStateFile) + } else { + pv = privval.GenFilePV(privValKeyFile, privValStateFile) + pv.Save() + logger.Info("Generated private validator", "keyFile", privValKeyFile, + "stateFile", privValStateFile) + } + + nodeKeyFile := config.NodeKeyFile() + if tmos.FileExists(nodeKeyFile) { + logger.Info("Found node key", "path", nodeKeyFile) + } else { + if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil { + return err + } + logger.Info("Generated node key", "path", nodeKeyFile) + } + + return nil +} diff --git a/gno.land/cmd/gnoland/main.go b/gno.land/cmd/gnoland/main.go new file mode 100644 index 00000000000..5310046c18a --- /dev/null +++ b/gno.land/cmd/gnoland/main.go @@ -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) + } +} diff --git a/gno.land/cmd/gnoland/mockio.go b/gno.land/cmd/gnoland/mockio.go new file mode 100644 index 00000000000..fd33eeab430 --- /dev/null +++ b/gno.land/cmd/gnoland/mockio.go @@ -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 MockStdin %v", i) + } + + // 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 + } +} diff --git a/gno.land/cmd/gnoland/node_id.go b/gno.land/cmd/gnoland/node_id.go new file mode 100644 index 00000000000..45be4df89ae --- /dev/null +++ b/gno.land/cmd/gnoland/node_id.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +// Display a node's persistent peer ID to the standard output. +func newNodeIDCmd(bc baseCfg) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "node", + ShortUsage: "node", + ShortHelp: "display the node id for configuring persistent peers", + }, + nil, + func(_ context.Context, args []string) error { + return execNodeID(bc) + }, + ) + return cmd +} + +func execNodeID(bc baseCfg) error { + config := bc.tmConfig + nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) + if err != nil { + return err + } + + fmt.Printf("NodeID: %v\n", nodeKey.ID()) + return nil +} diff --git a/gno.land/cmd/gnoland/reset.go b/gno.land/cmd/gnoland/reset.go new file mode 100644 index 00000000000..68dd19c8c72 --- /dev/null +++ b/gno.land/cmd/gnoland/reset.go @@ -0,0 +1,147 @@ +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 data, reset the node and 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 +} + +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, + ) + } +} + +// XXX: resetState is less risky than resetAll; however, it is still considered unsafe. +// resetState removes all databases but retains the last voting state and height of a validator. +// It is used by a validator to resync the state from other nodes, reducing the risk of double signing +// historical blocks during state syncs and the risk of a chain fork. +func newResetStateCmd(bc baseCfg) *commands.Command { + cfg := resetCfg{ + baseCfg: bc, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "reset-state", + ShortUsage: "reset-state", + ShortHelp: "reset node to genesis state, retaining validator state.", + }, + &cfg, + func(_ context.Context, args []string) error { + return execResetState(cfg, args) + }, + ) +} + +func execResetState(rc resetCfg, args []string) (err error) { + config := rc.tmConfig + + return resetState( + config.DBDir(), + logger, + ) +} + +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") + + removeData(blockdb) + removeData(state) + removeData(wal) + removeData(gnolang) + + if err := osm.EnsureDir(dbDir, 0o700); err != nil { + logger.Error("unable to recreate dbDir", "err", err) + } + return nil +} + +func removeData(filepath string) { + if osm.FileExists(filepath) { + if err := os.RemoveAll(filepath); err == nil { + logger.Info("Removed all", filepath) + } else { + logger.Error("error removing all", filepath, "err", err) + } + } +} diff --git a/gno.land/cmd/gnoland/reset_test.go b/gno.land/cmd/gnoland/reset_test.go new file mode 100644 index 00000000000..fab3af572a1 --- /dev/null +++ b/gno.land/cmd/gnoland/reset_test.go @@ -0,0 +1,97 @@ +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.NoFileExists(t, filepath.Join(config.DBDir(), "cs.wal")) + + 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 Test_ResetState(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, resetState(config.DBDir(), 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.NoFileExists(t, filepath.Join(config.DBDir(), "cs.wal")) + + require.FileExists(t, config.PrivValidatorStateFile()) + require.FileExists(t, config.GenesisFile()) + + pv = privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) + // private validator state should still be in tact. + require.Equal(t, int64(10), 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 +} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index cf2a6252478..e140304cd80 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -1,42 +1,59 @@ package main import ( - "context" - "fmt" + "flag" "os" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/peterbourgon/ff/v3" - "github.com/peterbourgon/ff/v3/fftoml" + "github.com/gnolang/gno/tm2/pkg/log" ) -func main() { - io := commands.NewDefaultIO() - cmd := newRootCmd(io) +var logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } +type baseCfg struct { + rootDir string + tmConfig tmcfg.Config } -func newRootCmd(io *commands.IO) *commands.Command { +func (bc *baseCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &bc.rootDir, + "root-dir", + "testdir", + "directory for config and data", + ) +} + +func newRootCmd() *commands.Command { + bc := baseCfg{} + cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", - ShortHelp: "Starts the gnoland blockchain node", - Options: []ff.Option{ - ff.WithConfigFileFlag("config"), - ff.WithConfigFileParser(fftoml.Parser), - }, + ShortHelp: "The gnoland blockchain node", }, - commands.NewEmptyConfig(), + &bc, commands.HelpExec, ) + initTmConfig(&bc) cmd.AddSubCommands( - newStartCmd(io), + newStartCmd(bc), + newInitCmd(bc), + newResetAllCmd(bc), + newResetStateCmd(bc), + newNodeIDCmd(bc), + newValidatorCmd(bc), ) return cmd } + +// we relies on the flag option to pass in the root directory before we can identify where +func initTmConfig(bc *baseCfg) error { + bc.tmConfig = *tmcfg.LoadOrMakeConfigWithOptions(bc.rootDir, func(cfg *tmcfg.Config) { + }) + + return nil +} diff --git a/gno.land/cmd/gnoland/show_validator.go b/gno.land/cmd/gnoland/show_validator.go new file mode 100644 index 00000000000..8d19dbeeb76 --- /dev/null +++ b/gno.land/cmd/gnoland/show_validator.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// Display a node's validator info. +func newValidatorCmd(bc baseCfg) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "validator", + ShortUsage: "validator", + ShortHelp: "Show node's validator info", + }, + nil, + func(_ context.Context, args []string) error { + return execShowValidator(bc) + }, + ) + return cmd +} + +func execShowValidator(bc baseCfg) error { + config := bc.tmConfig + keyFilePath := config.PrivValidatorKeyFile() + stateFilePath := config.PrivValidatorStateFile() + pv := privval.LoadFilePV(keyFilePath, stateFilePath) + pubKey := pv.GetPubKey() + bz := amino.MustMarshalJSON(pubKey) + + fmt.Printf("Address: \"%v\"\n", pubKey.Address()) + fmt.Printf(" Pubkey: %v\n", string(bz)) + return nil +} diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index b2134d86ea9..077d0ada55f 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -14,31 +14,35 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/privval" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/log" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store" ) type startCfg struct { + baseCfg skipFailingGenesisTxs bool skipStart bool genesisBalancesFile string genesisTxsFile string chainID string genesisRemote string - rootDir string genesisMaxVMCycles int64 + pruneStrategy string + persistentPeers string config string } -func newStartCmd(io *commands.IO) *commands.Command { - cfg := &startCfg{} +func newStartCmd(bc baseCfg) *commands.Command { + cfg := startCfg{ + baseCfg: bc, + } return commands.NewCommand( commands.Metadata{ @@ -46,9 +50,9 @@ func newStartCmd(io *commands.IO) *commands.Command { ShortUsage: "start [flags]", ShortHelp: "Run the full node", }, - cfg, + &cfg, func(_ context.Context, args []string) error { - return execStart(cfg, args, io) + return execStart(cfg, args) }, ) } @@ -89,13 +93,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "the ID of the chain", ) - fs.StringVar( - &c.rootDir, - "root-dir", - "testdir", - "directory for config and data", - ) - fs.StringVar( &c.genesisRemote, "genesis-remote", @@ -109,32 +106,33 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { 10_000_000, "set maximum allowed vm cycles per operation. Zero means no limit.", ) + fs.StringVar( + &c.pruneStrategy, + "prune", + "syncable", + "set the state prune strategy: nothing | everything | syncable", + ) fs.StringVar( - &c.config, - "config", + &c.persistentPeers, + "peers", "", - "config file (optional)", + "persistent peers: node_id@host:port,...", ) } -func execStart(c *startCfg, args []string, io *commands.IO) error { - logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) +func execStart(c startCfg, args []string) error { rootDir := c.rootDir - - cfg := config.LoadOrMakeConfigWithOptions(rootDir, func(cfg *config.Config) { - cfg.Consensus.CreateEmptyBlocks = true - cfg.Consensus.CreateEmptyBlocksInterval = 0 * time.Second - }) + tmcfg := &c.baseCfg.tmConfig // create priv validator first. // need it to generate genesis.json - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() + newPrivValKey := tmcfg.PrivValidatorKeyFile() + newPrivValState := tmcfg.PrivValidatorStateFile() priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) // write genesis file if missing. - genesisFilePath := filepath.Join(rootDir, cfg.Genesis) + genesisFilePath := filepath.Join(rootDir, tmcfg.Genesis) if !osm.FileExists(genesisFilePath) { genDoc := makeGenesisDoc( priv.GetPubKey(), @@ -151,17 +149,25 @@ func execStart(c *startCfg, args []string, io *commands.IO) error { return fmt.Errorf("error in creating new app: %w", err) } - cfg.LocalApp = gnoApp + // prune nothing is the archive node setting + // prune syncable is the default node setting. It keeps the lastest 100 transactions and everything 1000th tx + prune := store.NewPruningOptionsFromString(c.pruneStrategy) + pruningOpt := sdk.SetPruningOptions(prune) + gnoBaseApp := gnoApp.(*sdk.BaseApp) + pruningOpt(gnoBaseApp) + + tmcfg.LocalApp = gnoBaseApp + tmcfg.P2P.PersistentPeers = c.persistentPeers - gnoNode, err := node.DefaultNewNode(cfg, logger) + gnoNode, err := node.DefaultNewNode(tmcfg, logger) if err != nil { return fmt.Errorf("error in creating node: %w", err) } - fmt.Fprintln(io.Err, "Node created.") + fmt.Println("Node created.") if c.skipStart { - fmt.Fprintln(io.Err, "'--skip-start' is set. Exiting.") + fmt.Println("'--skip-start' is set. Exiting.") return nil } diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 27ef2f572ea..ac35f2d0b31 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -1,14 +1,12 @@ package main import ( - "bytes" "context" "os" "path/filepath" "strings" "testing" - "github.com/gnolang/gno/tm2/pkg/commands" "github.com/stretchr/testify/require" ) @@ -24,24 +22,29 @@ func TestStartInitialize(t *testing.T) { for _, tc := range cases { name := strings.Join(tc.args, " ") - t.Run(name, func(t *testing.T) { - mockOut := bytes.NewBufferString("") - mockErr := bytes.NewBufferString("") - io := commands.NewTestIO() - io.SetOut(commands.WriteNopCloser(mockOut)) - io.SetErr(commands.WriteNopCloser(mockErr)) - cmd := newRootCmd(io) + in, err := NewMockStdin(name) + defer in.Close() + if err != nil { + t.Fatal("failed creating test io pipe") + } + t.Run(name, func(t *testing.T) { + cmd := newRootCmd() t.Logf(`Running "gnoland %s"`, strings.Join(tc.args, " ")) err := cmd.ParseAndRun(context.Background(), tc.args) require.NoError(t, err) - stdout := mockOut.String() - stderr := mockErr.String() + bz, err := in.ReadAndClose() + if err != nil { + t.Fatal("failed reading test io pipe") + } + + out := string(bz) + + require.Contains(t, out, "Node created.", "failed to create node") - require.Contains(t, stderr, "Node created.", "failed to create node") - require.Contains(t, stderr, "'--skip-start' is set. Exiting.", "not exited with skip-start") - require.NotContains(t, stdout, "panic:") + require.Contains(t, out, "'--skip-start' is set. Exiting.", "not exited with skip-start") + require.NotContains(t, out, "panic:") }) } }