diff --git a/gno.land/cmd/gnoland/cmd_init.go b/gno.land/cmd/gnoland/cmd_init.go new file mode 100644 index 00000000000..b1aea40ddfb --- /dev/null +++ b/gno.land/cmd/gnoland/cmd_init.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "flag" + "fmt" + "path/filepath" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/config" + "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" +) + +func newInitCmd(io *commands.IO) *commands.Command { + cfg := &initCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "init", + ShortUsage: "init [flags]", + ShortHelp: "Initialize a gnoland configuration on file-system", + }, + cfg, + func(_ context.Context, args []string) error { + return execInit(cfg, args, io) + }, + ) +} + +type initCfg struct { + // common + rootDir string + config string + + // gnoland init specific + chainID string + genesisTxsFile string + genesisRemote string + skipFailingGenesisTxs bool + genesisBalancesFile string + genesisMaxVMCycles int64 +} + +func (c *initCfg) RegisterFlags(fs *flag.FlagSet) { + // common + fs.StringVar(&c.rootDir, "root-dir", "testdir", "directory for config and data") + fs.StringVar(&c.config, "config", "", "config file (optional)") + + // init specific + fs.BoolVar(&c.skipFailingGenesisTxs, "skip-failing-genesis-txs", false, "don't panic when replaying invalid genesis txs") + fs.StringVar(&c.genesisBalancesFile, "genesis-balances-file", "./genesis/genesis_balances.txt", "initial distribution file") + fs.StringVar(&c.genesisTxsFile, "genesis-txs-file", "./genesis/genesis_txs.txt", "initial txs to replay") + fs.StringVar(&c.chainID, "chainid", "dev", "the ID of the chain") + fs.StringVar(&c.genesisRemote, "genesis-remote", "localhost:26657", "replacement for '%%REMOTE%%' in genesis") + fs.Int64Var(&c.genesisMaxVMCycles, "genesis-max-vm-cycles", 10_000_000, "set maximum allowed vm cycles per operation. Zero means no limit.") +} + +func execInit(c *initCfg, args []string, io *commands.IO) error { + logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) + rootDir := c.rootDir + + cfg := config.LoadOrMakeConfigWithOptions(rootDir, func(cfg *config.Config) { + cfg.Consensus.CreateEmptyBlocks = true + cfg.Consensus.CreateEmptyBlocksInterval = 0 * time.Second + }) + + // create priv validator first. + // need it to generate genesis.json + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + + // write genesis file if missing. + genesisFilePath := filepath.Join(rootDir, cfg.Genesis) + if !osm.FileExists(genesisFilePath) { + genDoc := makeGenesisDoc( + priv.GetPubKey(), + c.chainID, + c.genesisBalancesFile, + loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote), + ) + writeGenesisFile(genDoc, genesisFilePath) + } + + // create application and node. + gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) + if err != nil { + return fmt.Errorf("error in creating new app: %w", err) + } + + cfg.LocalApp = gnoApp + + fmt.Fprintln(io.Err, "Node created.") + return nil +} diff --git a/gno.land/cmd/gnoland/cmd_start.go b/gno.land/cmd/gnoland/cmd_start.go new file mode 100644 index 00000000000..1247b636089 --- /dev/null +++ b/gno.land/cmd/gnoland/cmd_start.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/log" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +func newStartCmd(io *commands.IO) *commands.Command { + cfg := &startCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "start", + ShortUsage: "start [flags]", + ShortHelp: "Run the full node", + }, + cfg, + func(_ context.Context, args []string) error { + return execStart(cfg, args, io) + }, + ) +} + +type startCfg struct { + init initCfg +} + +func (s startCfg)RegisterFlags(fs *flag.FlagSet) { + s.init.RegisterFlags(fs) +} + +func execStart(c *startCfg, args []string, io *commands.IO) error { + rootDir := c.init.rootDir + logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) + + // lazy init + if !config.Exists(rootDir) { + err := execInit(&c.init, []string{}, io) // XXX: create an helper instead of calling the cmd? + if err != nil { + return fmt.Errorf("lazy init: %w", err) + } + } + + // load (existing) config + cfg := config.LoadOrMakeDefaultConfig(rootDir) + gnoApp, err := gnoland.NewApp(rootDir, c.init.skipFailingGenesisTxs, logger, c.init.genesisMaxVMCycles) + if err != nil { + return fmt.Errorf("error in creating new app: %w", err) + } + cfg.LocalApp = gnoApp + + // create node + gnoNode, err := node.DefaultNewNode(cfg, logger) + if err != nil { + return fmt.Errorf("error in creating node: %w", err) + } + + // start node + fmt.Fprintln(io.Err, "Starting Node...") + if err := gnoNode.Start(); err != nil { + return fmt.Errorf("error in start node: %w", err) + } + + // run forever + osm.TrapSignal(func() { + if gnoNode.IsRunning() { + _ = gnoNode.Stop() + } + }) + select {} // run forever +} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index cf2a6252478..0deaeb1c8de 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "os" @@ -24,7 +25,7 @@ func newRootCmd(io *commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", - ShortHelp: "Starts the gnoland blockchain node", + ShortHelp: "Manages the gnoland blockchain node", Options: []ff.Option{ ff.WithConfigFileFlag("config"), ff.WithConfigFileParser(fftoml.Parser), @@ -35,6 +36,7 @@ func newRootCmd(io *commands.IO) *commands.Command { ) cmd.AddSubCommands( + newInitCmd(io), newStartCmd(io), ) diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 27ef2f572ea..e9a1e0ef394 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -12,38 +12,27 @@ import ( "github.com/stretchr/testify/require" ) -func TestStartInitialize(t *testing.T) { - cases := []struct { - args []string - }{ - {[]string{"start", "--skip-start", "--skip-failing-genesis-txs"}}, - // {[]string{"--skip-start"}}, - // FIXME: test seems flappy as soon as we have multiple cases. - } +func TestInit(t *testing.T) { os.Chdir(filepath.Join("..", "..")) // go to repo's root dir - 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) - - 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() - - 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:") - }) - } + + mockOut := bytes.NewBufferString("") + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOut)) + io.SetErr(commands.WriteNopCloser(mockErr)) + cmd := newRootCmd(io) + + args := []string{"init", "--skip-failing-genesis-txs"} + t.Logf(`Running "gnoland %s"`, strings.Join(args, " ")) + err := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, err) + + stdout := mockOut.String() + stderr := mockErr.String() + + require.Contains(t, stderr, "Node created.") + require.NotContains(t, stdout, "panic:") } // TODO: test various configuration files? diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/util_genesis.go similarity index 51% rename from gno.land/cmd/gnoland/start.go rename to gno.land/cmd/gnoland/util_genesis.go index b2134d86ea9..1600d88b268 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/util_genesis.go @@ -1,8 +1,6 @@ package main import ( - "context" - "flag" "fmt" "path/filepath" "strings" @@ -14,172 +12,12 @@ 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/std" ) -type startCfg struct { - skipFailingGenesisTxs bool - skipStart bool - genesisBalancesFile string - genesisTxsFile string - chainID string - genesisRemote string - rootDir string - genesisMaxVMCycles int64 - config string -} - -func newStartCmd(io *commands.IO) *commands.Command { - cfg := &startCfg{} - - return commands.NewCommand( - commands.Metadata{ - Name: "start", - ShortUsage: "start [flags]", - ShortHelp: "Run the full node", - }, - cfg, - func(_ context.Context, args []string) error { - return execStart(cfg, args, io) - }, - ) -} - -func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { - fs.BoolVar( - &c.skipFailingGenesisTxs, - "skip-failing-genesis-txs", - false, - "don't panic when replaying invalid genesis txs", - ) - - fs.BoolVar( - &c.skipStart, - "skip-start", - false, - "quit after initialization, don't start the node", - ) - - fs.StringVar( - &c.genesisBalancesFile, - "genesis-balances-file", - "./genesis/genesis_balances.txt", - "initial distribution file", - ) - - fs.StringVar( - &c.genesisTxsFile, - "genesis-txs-file", - "./genesis/genesis_txs.txt", - "initial txs to replay", - ) - - fs.StringVar( - &c.chainID, - "chainid", - "dev", - "the ID of the chain", - ) - - fs.StringVar( - &c.rootDir, - "root-dir", - "testdir", - "directory for config and data", - ) - - fs.StringVar( - &c.genesisRemote, - "genesis-remote", - "localhost:26657", - "replacement for '%%REMOTE%%' in genesis", - ) - - fs.Int64Var( - &c.genesisMaxVMCycles, - "genesis-max-vm-cycles", - 10_000_000, - "set maximum allowed vm cycles per operation. Zero means no limit.", - ) - - fs.StringVar( - &c.config, - "config", - "", - "config file (optional)", - ) -} - -func execStart(c *startCfg, args []string, io *commands.IO) error { - logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) - rootDir := c.rootDir - - cfg := config.LoadOrMakeConfigWithOptions(rootDir, func(cfg *config.Config) { - cfg.Consensus.CreateEmptyBlocks = true - cfg.Consensus.CreateEmptyBlocksInterval = 0 * time.Second - }) - - // create priv validator first. - // need it to generate genesis.json - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - - // write genesis file if missing. - genesisFilePath := filepath.Join(rootDir, cfg.Genesis) - if !osm.FileExists(genesisFilePath) { - genDoc := makeGenesisDoc( - priv.GetPubKey(), - c.chainID, - c.genesisBalancesFile, - loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote), - ) - writeGenesisFile(genDoc, genesisFilePath) - } - - // create application and node. - gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) - if err != nil { - return fmt.Errorf("error in creating new app: %w", err) - } - - cfg.LocalApp = gnoApp - - gnoNode, err := node.DefaultNewNode(cfg, logger) - if err != nil { - return fmt.Errorf("error in creating node: %w", err) - } - - fmt.Fprintln(io.Err, "Node created.") - - if c.skipStart { - fmt.Fprintln(io.Err, "'--skip-start' is set. Exiting.") - - return nil - } - - if err := gnoNode.Start(); err != nil { - return fmt.Errorf("error in start node: %w", err) - } - - // run forever - osm.TrapSignal(func() { - if gnoNode.IsRunning() { - _ = gnoNode.Stop() - } - }) - - select {} // run forever -} - // Makes a local test genesis doc with local privValidator. func makeGenesisDoc( pvPub crypto.PubKey, diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 6f148c3b5c1..4d5cc13043b 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -65,6 +65,12 @@ func LoadOrMakeConfigWithOptions(root string, options ConfigOptions) (cfg *Confi return cfg } +// Exists checks if a configuration exists in the specified root dir. +func Exists(root string) bool { + configPath := join(root, defaultConfigFilePath) + return osm.FileExists(configPath) +} + // TestConfig returns a configuration that can be used for testing func TestConfig() *Config { return &Config{ @@ -240,6 +246,7 @@ func DefaultBaseConfig() BaseConfig { FilterPeers: false, DBBackend: "goleveldb", DBPath: "data", + // LocalApp: abci.Application, } }