From 4c9d10c218ac16965259b3063899d51ad48a92a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Tue, 12 Mar 2024 12:06:27 +0100 Subject: [PATCH] feat: add `gnoland config` command suite (#1605) ## Description Based on discussions in #1593, this PR introduces the CLI suite for manipulating the `config.toml`. Using this suite, we can build better workflows for power users. This PR is a series of lego blocks that are required for us to get a normal user chain connection going: - #1252 - solved `genesis.json` management and manipulation - #1593 - solved node secrets management and manipulation - this PR - solves `config.toml` management and manipulation All of these PRs get us to a point where the user can run: - `gnoland init` - `gnoland start` to get up and running with any Gno network. The added middle step is fine-tuning the configuration and genesis, but it's worth noting this step is optional. New commands: ```shell gnoland config --help USAGE config [flags] Gno config manipulation suite, for editing base and module configurations SUBCOMMANDS init Initializes the Gno node configuration set Edits the Gno node configuration get Shows the Gno node configuration ``` In short, the `gnoland config init` command initializes a default `config.toml`, while other subcommands allow editing viewing any field in the specific configuration.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- gno.land/cmd/genesis/balances_add_test.go | 6 +- gno.land/cmd/gnoland/config.go | 94 +++ gno.land/cmd/gnoland/config_get.go | 73 ++ gno.land/cmd/gnoland/config_get_test.go | 650 ++++++++++++++++ gno.land/cmd/gnoland/config_init.go | 52 ++ gno.land/cmd/gnoland/config_init_test.go | 61 ++ gno.land/cmd/gnoland/config_set.go | 172 +++++ gno.land/cmd/gnoland/config_set_test.go | 900 ++++++++++++++++++++++ gno.land/cmd/gnoland/root.go | 5 +- gno.land/cmd/gnoland/start.go | 4 +- gno.land/cmd/gnoland/start_test.go | 4 +- tm2/pkg/bft/config/config.go | 25 +- tm2/pkg/db/db.go | 4 + 13 files changed, 2027 insertions(+), 23 deletions(-) create mode 100644 gno.land/cmd/gnoland/config.go create mode 100644 gno.land/cmd/gnoland/config_get.go create mode 100644 gno.land/cmd/gnoland/config_get_test.go create mode 100644 gno.land/cmd/gnoland/config_init.go create mode 100644 gno.land/cmd/gnoland/config_init_test.go create mode 100644 gno.land/cmd/gnoland/config_set.go create mode 100644 gno.land/cmd/gnoland/config_set_test.go diff --git a/gno.land/cmd/genesis/balances_add_test.go b/gno.land/cmd/genesis/balances_add_test.go index 73e2fe148a2..0dd3366869f 100644 --- a/gno.land/cmd/genesis/balances_add_test.go +++ b/gno.land/cmd/genesis/balances_add_test.go @@ -581,7 +581,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { marshalledTxs = append(marshalledTxs, string(marshalledTx)) } - mockErr := bytes.NewBufferString("") + mockErr := new(bytes.Buffer) io := commands.NewTestIO() io.SetErr(commands.WriteNopCloser(mockErr)) @@ -638,7 +638,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { marshalledTxs = append(marshalledTxs, string(marshalledTx)) } - mockErr := bytes.NewBufferString("") + mockErr := new(bytes.Buffer) io := commands.NewTestIO() io.SetErr(commands.WriteNopCloser(mockErr)) @@ -690,7 +690,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { marshalledTxs = append(marshalledTxs, string(marshalledTx)) } - mockErr := bytes.NewBufferString("") + mockErr := new(bytes.Buffer) io := commands.NewTestIO() io.SetErr(commands.WriteNopCloser(mockErr)) diff --git a/gno.land/cmd/gnoland/config.go b/gno.land/cmd/gnoland/config.go new file mode 100644 index 00000000000..d5b61ea48b6 --- /dev/null +++ b/gno.land/cmd/gnoland/config.go @@ -0,0 +1,94 @@ +package main + +import ( + "flag" + "fmt" + "reflect" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type configCfg struct { + configPath string +} + +// newConfigCmd creates the config root command +func newConfigCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "config", + ShortUsage: "config [flags]", + ShortHelp: "gno config manipulation suite", + LongHelp: "Gno config manipulation suite, for editing base and module configurations", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newConfigInitCmd(io), + newConfigSetCmd(io), + newConfigGetCmd(io), + ) + + return cmd +} + +func (c *configCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.configPath, + "config-path", + "./config.toml", + "the path for the config.toml", + ) +} + +// getFieldAtPath fetches the given field from the given path +func getFieldAtPath(currentValue reflect.Value, path []string) (*reflect.Value, error) { + // Look at the current section, and figure out if + // it's a part of the current struct + field := currentValue.FieldByName(path[0]) + if !field.IsValid() || !field.CanSet() { + return nil, newInvalidFieldError(path[0], currentValue) + } + + // Dereference the field if needed + if field.Kind() == reflect.Ptr { + field = field.Elem() + } + + // Check if this is not the end of the path + // ex: x.y.field + if len(path) > 1 { + // Recursively try to traverse the path and return the given field + return getFieldAtPath(field, path[1:]) + } + + return &field, nil +} + +// newInvalidFieldError creates an error for non-existent struct fields +// being passed as arguments to [getFieldAtPath] +func newInvalidFieldError(field string, value reflect.Value) error { + var ( + valueType = value.Type() + numFields = value.NumField() + ) + + fields := make([]string, 0, numFields) + + for i := 0; i < numFields; i++ { + valueField := valueType.Field(i) + if !valueField.IsExported() { + continue + } + + fields = append(fields, valueField.Name) + } + + return fmt.Errorf( + "field %q, is not a valid configuration key, available keys: %s", + field, + fields, + ) +} diff --git a/gno.land/cmd/gnoland/config_get.go b/gno.land/cmd/gnoland/config_get.go new file mode 100644 index 00000000000..ffcb739be9c --- /dev/null +++ b/gno.land/cmd/gnoland/config_get.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errInvalidConfigGetArgs = errors.New("invalid number of config get arguments provided") + +// newConfigGetCmd creates the config get command +func newConfigGetCmd(io commands.IO) *commands.Command { + cfg := &configCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "get", + ShortUsage: "config get ", + ShortHelp: "shows the Gno node configuration", + LongHelp: "Shows the Gno node configuration at the given path " + + "by fetching the option specified at ", + }, + cfg, + func(_ context.Context, args []string) error { + return execConfigGet(cfg, io, args) + }, + ) + + return cmd +} + +func execConfigGet(cfg *configCfg, io commands.IO, args []string) error { + // Load the config + loadedCfg, err := config.LoadConfigFile(cfg.configPath) + if err != nil { + return fmt.Errorf("unable to load config, %w", err) + } + + // Make sure the edit arguments are valid + if len(args) != 1 { + return errInvalidConfigGetArgs + } + + // Find and print the config field, if any + if err := printConfigField(loadedCfg, args[0], io); err != nil { + return fmt.Errorf("unable to update config field, %w", err) + } + + return nil +} + +// printConfigField prints the value of the field at the given path +func printConfigField(config *config.Config, key string, io commands.IO) error { + // Get the config value using reflect + configValue := reflect.ValueOf(config).Elem() + + // Get the value path, with sections separated out by a period + path := strings.Split(key, ".") + + field, err := getFieldAtPath(configValue, path) + if err != nil { + return err + } + + io.Printf("%v", field.Interface()) + + return nil +} diff --git a/gno.land/cmd/gnoland/config_get_test.go b/gno.land/cmd/gnoland/config_get_test.go new file mode 100644 index 00000000000..93baa3d20ba --- /dev/null +++ b/gno.land/cmd/gnoland/config_get_test.go @@ -0,0 +1,650 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "strconv" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig_Get_Invalid(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "get", + "--config-path", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to load config") +} + +// testSetCase outlines the single test case for config get +type testGetCase struct { + name string + field string + verifyFn func(*config.Config, string) +} + +// verifyGetTestTableCommon is the common test table +// verification for config set test cases +func verifyGetTestTableCommon(t *testing.T, testTable []testGetCase) { + t.Helper() + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + // Setup the test config + path := initializeTestConfig(t) + args := []string{ + "config", + "get", + "--config-path", + path, + } + + // Create the command IO + mockOut := new(bytes.Buffer) + + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOut)) + + // Create the command + cmd := newRootCmd(io) + args = append(args, testCase.field) + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Make sure the config was fetched + loadedCfg, err := config.LoadConfigFile(path) + require.NoError(t, err) + + testCase.verifyFn(loadedCfg, mockOut.String()) + }) + } +} + +func TestConfig_Get_Base(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "root dir fetched", + "RootDir", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.RootDir, value) + }, + }, + { + "proxy app fetched", + "ProxyApp", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.ProxyApp, value) + }, + }, + { + "moniker fetched", + "Moniker", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.Moniker, value) + }, + }, + { + "fast sync mode fetched", + "FastSyncMode", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, loadedCfg.FastSyncMode, boolVal) + }, + }, + { + "db backend fetched", + "DBBackend", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.DBBackend, value) + }, + }, + { + "db path fetched", + "DBPath", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.DBPath, value) + }, + }, + { + "genesis path fetched", + "Genesis", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.Genesis, value) + }, + }, + { + "validator key fetched", + "PrivValidatorKey", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.PrivValidatorKey, value) + }, + }, + { + "validator state file fetched", + "PrivValidatorState", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.PrivValidatorState, value) + }, + }, + { + "validator listen addr fetched", + "PrivValidatorListenAddr", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.PrivValidatorListenAddr, value) + }, + }, + { + "node key path fetched", + "NodeKey", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.NodeKey, value) + }, + }, + { + "abci fetched", + "ABCI", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.ABCI, value) + }, + }, + { + "profiling listen address fetched", + "ProfListenAddress", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, loadedCfg.ProfListenAddress, value) + }, + }, + { + "filter peers flag fetched", + "FilterPeers", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, loadedCfg.FilterPeers, boolVal) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} + +func TestConfig_Get_Consensus(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "root dir updated", + "Consensus.RootDir", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.RootDir) + }, + }, + { + "WAL path updated", + "Consensus.WALPath", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.WALPath) + }, + }, + { + "propose timeout updated", + "Consensus.TimeoutPropose", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPropose.String()) + }, + }, + { + "propose timeout delta updated", + "Consensus.TimeoutProposeDelta", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutProposeDelta.String()) + }, + }, + { + "prevote timeout updated", + "Consensus.TimeoutPrevote", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrevote.String()) + }, + }, + { + "prevote timeout delta updated", + "Consensus.TimeoutPrevoteDelta", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrevoteDelta.String()) + }, + }, + { + "precommit timeout updated", + "Consensus.TimeoutPrecommit", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrecommit.String()) + }, + }, + { + "precommit timeout delta updated", + "Consensus.TimeoutPrecommitDelta", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrecommitDelta.String()) + }, + }, + { + "commit timeout updated", + "Consensus.TimeoutCommit", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutCommit.String()) + }, + }, + { + "skip commit timeout toggle updated", + "Consensus.SkipTimeoutCommit", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.Consensus.SkipTimeoutCommit) + }, + }, + { + "create empty blocks toggle updated", + "Consensus.CreateEmptyBlocks", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + assert.Equal(t, boolVal, loadedCfg.Consensus.CreateEmptyBlocks) + }, + }, + { + "create empty blocks interval updated", + "Consensus.CreateEmptyBlocksInterval", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.CreateEmptyBlocksInterval.String()) + }, + }, + { + "peer gossip sleep duration updated", + "Consensus.PeerGossipSleepDuration", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.PeerGossipSleepDuration.String()) + }, + }, + { + "peer query majority sleep duration updated", + "Consensus.PeerQueryMaj23SleepDuration", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.PeerQueryMaj23SleepDuration.String()) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} + +func TestConfig_Get_Events(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "event store type updated", + "TxEventStore.EventStoreType", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.TxEventStore.EventStoreType) + }, + }, + { + "event store params updated", + "TxEventStore.Params", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%v", loadedCfg.TxEventStore.Params)) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} + +func TestConfig_Get_P2P(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "root dir updated", + "P2P.RootDir", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.RootDir) + }, + }, + { + "listen address updated", + "P2P.ListenAddress", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.ListenAddress) + }, + }, + { + "external address updated", + "P2P.ExternalAddress", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.ExternalAddress) + }, + }, + { + "seeds updated", + "P2P.Seeds", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.Seeds) + }, + }, + { + "persistent peers updated", + "P2P.PersistentPeers", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.PersistentPeers) + }, + }, + { + "upnp toggle updated", + "P2P.UPNP", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.UPNP) + }, + }, + { + "max inbound peers updated", + "P2P.MaxNumInboundPeers", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxNumInboundPeers)) + }, + }, + { + "max outbound peers updated", + "P2P.MaxNumOutboundPeers", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxNumOutboundPeers)) + }, + }, + { + "flush throttle timeout updated", + "P2P.FlushThrottleTimeout", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.FlushThrottleTimeout.String()) + }, + }, + { + "max package payload size updated", + "P2P.MaxPacketMsgPayloadSize", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxPacketMsgPayloadSize)) + }, + }, + { + "send rate updated", + "P2P.SendRate", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.SendRate)) + }, + }, + { + "receive rate updated", + "P2P.RecvRate", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.RecvRate)) + }, + }, + { + "pex reactor toggle updated", + "P2P.PexReactor", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.PexReactor) + }, + }, + { + "seed mode updated", + "P2P.SeedMode", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.SeedMode) + }, + }, + { + "private peer IDs updated", + "P2P.PrivatePeerIDs", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.PrivatePeerIDs) + }, + }, + { + "allow duplicate IPs updated", + "P2P.AllowDuplicateIP", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.AllowDuplicateIP) + }, + }, + { + "handshake timeout updated", + "P2P.HandshakeTimeout", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.HandshakeTimeout.String()) + }, + }, + { + "dial timeout updated", + "P2P.DialTimeout", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.DialTimeout.String()) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} + +func TestConfig_Get_RPC(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "root dir updated", + "RPC.RootDir", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.RootDir) + }, + }, + { + "listen address updated", + "RPC.ListenAddress", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.ListenAddress) + }, + }, + { + "CORS Allowed Origins updated", + "RPC.CORSAllowedOrigins", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%v", loadedCfg.RPC.CORSAllowedOrigins)) + }, + }, + { + "CORS Allowed Methods updated", + "RPC.CORSAllowedMethods", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%v", loadedCfg.RPC.CORSAllowedMethods)) + }, + }, + { + "CORS Allowed Headers updated", + "RPC.CORSAllowedHeaders", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%v", loadedCfg.RPC.CORSAllowedHeaders)) + }, + }, + { + "GRPC listen address updated", + "RPC.GRPCListenAddress", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.GRPCListenAddress) + }, + }, + { + "GRPC max open connections updated", + "RPC.GRPCMaxOpenConnections", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.GRPCMaxOpenConnections)) + }, + }, + { + "unsafe value updated", + "RPC.Unsafe", + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.RPC.Unsafe) + }, + }, + { + "RPC max open connections updated", + "RPC.MaxOpenConnections", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxOpenConnections)) + }, + }, + { + "tx commit broadcast timeout updated", + "RPC.TimeoutBroadcastTxCommit", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TimeoutBroadcastTxCommit.String()) + }, + }, + { + "max body bytes updated", + "RPC.MaxBodyBytes", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxBodyBytes)) + }, + }, + { + "max header bytes updated", + "RPC.MaxHeaderBytes", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxHeaderBytes)) + }, + }, + { + "TLS cert file updated", + "RPC.TLSCertFile", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TLSCertFile) + }, + }, + { + "TLS key file updated", + "RPC.TLSKeyFile", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TLSKeyFile) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} + +func TestConfig_Get_Mempool(t *testing.T) { + t.Parallel() + + testTable := []testGetCase{ + { + "root dir updated", + "Mempool.RootDir", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Mempool.RootDir) + }, + }, + { + "recheck flag updated", + "Mempool.Recheck", + func(loadedCfg *config.Config, value string) { + boolVar, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVar, loadedCfg.Mempool.Recheck) + }, + }, + { + "broadcast flag updated", + "Mempool.Broadcast", + func(loadedCfg *config.Config, value string) { + boolVar, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVar, loadedCfg.Mempool.Broadcast) + }, + }, + { + "WAL path updated", + "Mempool.WalPath", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Mempool.WalPath) + }, + }, + { + "size updated", + "Mempool.Size", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.Size)) + }, + }, + { + "max pending txs bytes updated", + "Mempool.MaxPendingTxsBytes", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.MaxPendingTxsBytes)) + }, + }, + { + "cache size updated", + "Mempool.CacheSize", + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.CacheSize)) + }, + }, + } + + verifyGetTestTableCommon(t, testTable) +} diff --git a/gno.land/cmd/gnoland/config_init.go b/gno.land/cmd/gnoland/config_init.go new file mode 100644 index 00000000000..be7902b48cd --- /dev/null +++ b/gno.land/cmd/gnoland/config_init.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errInvalidConfigOutputPath = errors.New("invalid config output path provided") + +// newConfigInitCmd creates the config init command +func newConfigInitCmd(io commands.IO) *commands.Command { + cfg := &configCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "init", + ShortUsage: "config init [flags]", + ShortHelp: "initializes the Gno node configuration", + LongHelp: "Initializes the Gno node configuration locally with default values, which includes" + + " the base and module configurations", + }, + cfg, + func(_ context.Context, _ []string) error { + return execConfigInit(cfg, io) + }, + ) + + return cmd +} + +func execConfigInit(cfg *configCfg, io commands.IO) error { + // Check the config output path + if cfg.configPath == "" { + return errInvalidConfigOutputPath + } + + // Get the default config + c := config.DefaultConfig() + + // Save the config to the path + if err := config.WriteConfigFile(cfg.configPath, c); err != nil { + return fmt.Errorf("unable to initialize config, %w", err) + } + + io.Printfln("Default configuration initialized at %s", cfg.configPath) + + return nil +} diff --git a/gno.land/cmd/gnoland/config_init_test.go b/gno.land/cmd/gnoland/config_init_test.go new file mode 100644 index 00000000000..c576aec1641 --- /dev/null +++ b/gno.land/cmd/gnoland/config_init_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfig_Init(t *testing.T) { + t.Parallel() + + t.Run("invalid output path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "init", + "--config-path", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidConfigOutputPath.Error()) + }) + + t.Run("default config initialized", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + path := filepath.Join(tempDir, "config.toml") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "init", + "--config-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the config is valid + cfg, err := config.LoadConfigFile(path) + require.NoError(t, err) + + assert.NoError(t, cfg.ValidateBasic()) + assert.Equal(t, cfg, config.DefaultConfig()) + }) +} diff --git a/gno.land/cmd/gnoland/config_set.go b/gno.land/cmd/gnoland/config_set.go new file mode 100644 index 00000000000..47aa2e6559a --- /dev/null +++ b/gno.land/cmd/gnoland/config_set.go @@ -0,0 +1,172 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "time" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errInvalidConfigSetArgs = errors.New("invalid number of config set arguments provided") + +// newConfigSetCmd creates the config set command +func newConfigSetCmd(io commands.IO) *commands.Command { + cfg := &configCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "set", + ShortUsage: "config set ", + ShortHelp: "edits the Gno node configuration", + LongHelp: "Edits the Gno node configuration at the given path " + + "by setting the option specified at to the given ", + }, + cfg, + func(_ context.Context, args []string) error { + return execConfigEdit(cfg, io, args) + }, + ) + + return cmd +} + +func execConfigEdit(cfg *configCfg, io commands.IO, args []string) error { + // Load the config + loadedCfg, err := config.LoadConfigFile(cfg.configPath) + if err != nil { + return fmt.Errorf("unable to load config, %w", err) + } + + // Make sure the edit arguments are valid + if len(args) != 2 { + return errInvalidConfigSetArgs + } + + var ( + key = args[0] + value = args[1] + ) + + // Update the config field + if err := updateConfigField( + loadedCfg, + key, + value, + ); err != nil { + return fmt.Errorf("unable to update config field, %w", err) + } + + // Make sure the config is now valid + if err := loadedCfg.ValidateBasic(); err != nil { + return fmt.Errorf("unable to validate config, %w", err) + } + + // Save the config + if err := config.WriteConfigFile(cfg.configPath, loadedCfg); err != nil { + return fmt.Errorf("unable to save updated config, %w", err) + } + + io.Printfln("Updated configuration saved at %s", cfg.configPath) + + return nil +} + +// updateFieldAtPath updates the field at the given path, with the given value +func updateConfigField(config *config.Config, key, value string) error { + // Get the config value using reflect + configValue := reflect.ValueOf(config).Elem() + + // Get the value path, with sections separated out by a period + path := strings.Split(key, ".") + + // Get the editable field + field, err := getFieldAtPath(configValue, path) + if err != nil { + return err + } + + // Attempt to update the field value + if err = saveStringToValue(value, *field); err != nil { + return fmt.Errorf("unable to convert value to field type, %w", err) + } + + return nil +} + +// saveStringToValue attempts to convert the given +// string value to the destination type and save it to the destination value. +// Because we opted to using reflect instead of a flag-based approach, +// arguments (always strings) need to be converted to the field's +// respective type, if possible +func saveStringToValue(value string, dstValue reflect.Value) error { + switch dstValue.Interface().(type) { + case string: + dstValue.Set(reflect.ValueOf(value)) + case []string: + // This is a special case. + // Since values are given as a single string (argument), + // they need to be parsed from a custom format. + // In this case, the format for a []string is comma separated: + // value1,value2,value3 ... + val := strings.SplitN(value, ",", -1) + + dstValue.Set(reflect.ValueOf(val)) + case time.Duration: + val, err := time.ParseDuration(value) + if err != nil { + return fmt.Errorf("unable to parse time.Duration, %w", err) + } + + dstValue.Set(reflect.ValueOf(val)) + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: + return json.Unmarshal([]byte(value), dstValue.Addr().Interface()) + case types.EventStoreParams: + // This is a special case. + // Map values are tricky to parse, especially + // since it's a custom type alias, so this + // method is used to parse out the key value pairs + // that are given in a custom format, + // for the event store params + val := parseEventStoreParams(value) + + dstValue.Set(reflect.ValueOf(val)) + default: + return fmt.Errorf("unsupported type, %s", dstValue.Type().Name()) + } + + return nil +} + +// parseEventStoreParams parses the event store params into a param map. +// Map values are provided in the format = and comma separated +// for different keys: =,= +func parseEventStoreParams(values string) types.EventStoreParams { + params := make(types.EventStoreParams, len(values)) + + // Split the string into different key value pairs + keyPairs := strings.SplitN(values, ",", -1) + + for _, keyPair := range keyPairs { + // Split the string into key and value + kv := strings.SplitN(keyPair, "=", 2) + + // Check if the split produced exactly two elements + if len(kv) != 2 { + continue + } + + key := kv[0] + value := kv[1] + + params[key] = value + } + + return params +} diff --git a/gno.land/cmd/gnoland/config_set_test.go b/gno.land/cmd/gnoland/config_set_test.go new file mode 100644 index 00000000000..5824fa0ee06 --- /dev/null +++ b/gno.land/cmd/gnoland/config_set_test.go @@ -0,0 +1,900 @@ +package main + +import ( + "context" + "fmt" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// initializeTestConfig initializes a default configuration +// at a temporary path +func initializeTestConfig(t *testing.T) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "config.toml") + cfg := config.DefaultConfig() + + require.NoError(t, config.WriteConfigFile(path, cfg)) + + return path +} + +// testSetCase outlines the single test case for config set +type testSetCase struct { + name string + flags []string + verifyFn func(*config.Config, string) +} + +// verifySetTestTableCommon is the common test table +// verification for config set test cases +func verifySetTestTableCommon(t *testing.T, testTable []testSetCase) { + t.Helper() + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + // Setup the test config + path := initializeTestConfig(t) + args := []string{ + "config", + "set", + "--config-path", + path, + } + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args = append(args, testCase.flags...) + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Make sure the config was updated + loadedCfg, err := config.LoadConfigFile(path) + require.NoError(t, err) + + testCase.verifyFn(loadedCfg, testCase.flags[len(testCase.flags)-1]) + }) + } +} + +func TestConfig_Set_Invalid(t *testing.T) { + t.Parallel() + + t.Run("invalid config path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "set", + "--config-path", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to load config") + }) + + t.Run("invalid config change", func(t *testing.T) { + t.Parallel() + + // Setup the test config + path := initializeTestConfig(t) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "config", + "set", + "--config-path", + path, + "DBBackend", + "random db backend", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to validate config") + }) +} + +func TestConfig_Set_Base(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "root dir updated", + []string{ + "RootDir", + "example root dir", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RootDir) + }, + }, + { + "proxy app updated", + []string{ + "ProxyApp", + "example proxy app", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.ProxyApp) + }, + }, + { + "moniker updated", + []string{ + "Moniker", + "example moniker", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Moniker) + }, + }, + { + "fast sync mode updated", + []string{ + "FastSyncMode", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.FastSyncMode) + }, + }, + { + "db backend updated", + []string{ + "DBBackend", + db.GoLevelDBBackend.String(), + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.DBBackend) + }, + }, + { + "db path updated", + []string{ + "DBPath", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.DBPath) + }, + }, + { + "genesis path updated", + []string{ + "Genesis", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Genesis) + }, + }, + { + "validator key updated", + []string{ + "PrivValidatorKey", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.PrivValidatorKey) + }, + }, + { + "validator state file updated", + []string{ + "PrivValidatorState", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.PrivValidatorState) + }, + }, + { + "validator listen addr updated", + []string{ + "PrivValidatorListenAddr", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.PrivValidatorListenAddr) + }, + }, + { + "node key path updated", + []string{ + "NodeKey", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.NodeKey) + }, + }, + { + "abci updated", + []string{ + "ABCI", + config.LocalABCI, + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.ABCI) + }, + }, + { + "profiling listen address updated", + []string{ + "ProfListenAddress", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.ProfListenAddress) + }, + }, + { + "filter peers flag updated", + []string{ + "FilterPeers", + "true", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.FilterPeers) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} + +func TestConfig_Set_Consensus(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "root dir updated", + []string{ + "Consensus.RootDir", + "example root dir", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.RootDir) + }, + }, + { + "WAL path updated", + []string{ + "Consensus.WALPath", + "example WAL path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.WALPath) + }, + }, + { + "propose timeout updated", + []string{ + "Consensus.TimeoutPropose", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPropose.String()) + }, + }, + { + "propose timeout delta updated", + []string{ + "Consensus.TimeoutProposeDelta", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutProposeDelta.String()) + }, + }, + { + "prevote timeout updated", + []string{ + "Consensus.TimeoutPrevote", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrevote.String()) + }, + }, + { + "prevote timeout delta updated", + []string{ + "Consensus.TimeoutPrevoteDelta", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrevoteDelta.String()) + }, + }, + { + "precommit timeout updated", + []string{ + "Consensus.TimeoutPrecommit", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrecommit.String()) + }, + }, + { + "precommit timeout delta updated", + []string{ + "Consensus.TimeoutPrecommitDelta", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutPrecommitDelta.String()) + }, + }, + { + "commit timeout updated", + []string{ + "Consensus.TimeoutCommit", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.TimeoutCommit.String()) + }, + }, + { + "skip commit timeout toggle updated", + []string{ + "Consensus.SkipTimeoutCommit", + "true", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.Consensus.SkipTimeoutCommit) + }, + }, + { + "create empty blocks toggle updated", + []string{ + "Consensus.CreateEmptyBlocks", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + assert.Equal(t, boolVal, loadedCfg.Consensus.CreateEmptyBlocks) + }, + }, + { + "create empty blocks interval updated", + []string{ + "Consensus.CreateEmptyBlocksInterval", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.CreateEmptyBlocksInterval.String()) + }, + }, + { + "peer gossip sleep duration updated", + []string{ + "Consensus.PeerGossipSleepDuration", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.PeerGossipSleepDuration.String()) + }, + }, + { + "peer query majority sleep duration updated", + []string{ + "Consensus.PeerQueryMaj23SleepDuration", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Consensus.PeerQueryMaj23SleepDuration.String()) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} + +func TestConfig_Set_Events(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "event store type updated", + []string{ + "TxEventStore.EventStoreType", + file.EventStoreType, + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.TxEventStore.EventStoreType) + }, + }, + { + "event store params updated", + []string{ + "TxEventStore.Params", + "key1=value1,key2=value2", + }, + func(loadedCfg *config.Config, value string) { + val, ok := loadedCfg.TxEventStore.Params["key1"] + assert.True(t, ok) + assert.Equal(t, "value1", val) + + val, ok = loadedCfg.TxEventStore.Params["key2"] + assert.True(t, ok) + assert.Equal(t, "value2", val) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} + +func TestConfig_Set_P2P(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "root dir updated", + []string{ + "P2P.RootDir", + "example root dir", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.RootDir) + }, + }, + { + "listen address updated", + []string{ + "P2P.ListenAddress", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.ListenAddress) + }, + }, + { + "external address updated", + []string{ + "P2P.ExternalAddress", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.ExternalAddress) + }, + }, + { + "seeds updated", + []string{ + "P2P.Seeds", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.Seeds) + }, + }, + { + "persistent peers updated", + []string{ + "P2P.PersistentPeers", + "nodeID@0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.PersistentPeers) + }, + }, + { + "upnp toggle updated", + []string{ + "P2P.UPNP", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.UPNP) + }, + }, + { + "max inbound peers updated", + []string{ + "P2P.MaxNumInboundPeers", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxNumInboundPeers)) + }, + }, + { + "max outbound peers updated", + []string{ + "P2P.MaxNumOutboundPeers", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxNumOutboundPeers)) + }, + }, + { + "flush throttle timeout updated", + []string{ + "P2P.FlushThrottleTimeout", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.FlushThrottleTimeout.String()) + }, + }, + { + "max package payload size updated", + []string{ + "P2P.MaxPacketMsgPayloadSize", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.MaxPacketMsgPayloadSize)) + }, + }, + { + "send rate updated", + []string{ + "P2P.SendRate", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.SendRate)) + }, + }, + { + "receive rate updated", + []string{ + "P2P.RecvRate", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.P2P.RecvRate)) + }, + }, + { + "pex reactor toggle updated", + []string{ + "P2P.PexReactor", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.PexReactor) + }, + }, + { + "seed mode updated", + []string{ + "P2P.SeedMode", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.SeedMode) + }, + }, + { + "private peer IDs updated", + []string{ + "P2P.PrivatePeerIDs", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.PrivatePeerIDs) + }, + }, + { + "allow duplicate IPs updated", + []string{ + "P2P.AllowDuplicateIP", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.P2P.AllowDuplicateIP) + }, + }, + { + "handshake timeout updated", + []string{ + "P2P.HandshakeTimeout", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.HandshakeTimeout.String()) + }, + }, + { + "dial timeout updated", + []string{ + "P2P.DialTimeout", + "1s", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.P2P.DialTimeout.String()) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} + +func TestConfig_Set_RPC(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "root dir updated", + []string{ + "RPC.RootDir", + "example root dir", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.RootDir) + }, + }, + { + "listen address updated", + []string{ + "RPC.ListenAddress", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.ListenAddress) + }, + }, + { + "CORS Allowed Origins updated", + []string{ + "RPC.CORSAllowedOrigins", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, strings.SplitN(value, ",", -1), loadedCfg.RPC.CORSAllowedOrigins) + }, + }, + { + "CORS Allowed Methods updated", + []string{ + "RPC.CORSAllowedMethods", + "POST,GET", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, strings.SplitN(value, ",", -1), loadedCfg.RPC.CORSAllowedMethods) + }, + }, + { + "CORS Allowed Headers updated", + []string{ + "RPC.CORSAllowedHeaders", + "*", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, strings.SplitN(value, ",", -1), loadedCfg.RPC.CORSAllowedHeaders) + }, + }, + { + "GRPC listen address updated", + []string{ + "RPC.GRPCListenAddress", + "0.0.0.0:0", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.GRPCListenAddress) + }, + }, + { + "GRPC max open connections updated", + []string{ + "RPC.GRPCMaxOpenConnections", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.GRPCMaxOpenConnections)) + }, + }, + { + "unsafe value updated", + []string{ + "RPC.Unsafe", + "true", + }, + func(loadedCfg *config.Config, value string) { + boolVal, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVal, loadedCfg.RPC.Unsafe) + }, + }, + { + "RPC max open connections updated", + []string{ + "RPC.MaxOpenConnections", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxOpenConnections)) + }, + }, + { + "tx commit broadcast timeout updated", + []string{ + "RPC.TimeoutBroadcastTxCommit", + (time.Second * 10).String(), + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TimeoutBroadcastTxCommit.String()) + }, + }, + { + "max body bytes updated", + []string{ + "RPC.MaxBodyBytes", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxBodyBytes)) + }, + }, + { + "max header bytes updated", + []string{ + "RPC.MaxHeaderBytes", + "10", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.RPC.MaxHeaderBytes)) + }, + }, + { + "TLS cert file updated", + []string{ + "RPC.TLSCertFile", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TLSCertFile) + }, + }, + { + "TLS key file updated", + []string{ + "RPC.TLSKeyFile", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.RPC.TLSKeyFile) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} + +func TestConfig_Set_Mempool(t *testing.T) { + t.Parallel() + + testTable := []testSetCase{ + { + "root dir updated", + []string{ + "Mempool.RootDir", + "example root dir", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Mempool.RootDir) + }, + }, + { + "recheck flag updated", + []string{ + "Mempool.Recheck", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVar, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVar, loadedCfg.Mempool.Recheck) + }, + }, + { + "broadcast flag updated", + []string{ + "Mempool.Broadcast", + "false", + }, + func(loadedCfg *config.Config, value string) { + boolVar, err := strconv.ParseBool(value) + require.NoError(t, err) + + assert.Equal(t, boolVar, loadedCfg.Mempool.Broadcast) + }, + }, + { + "WAL path updated", + []string{ + "Mempool.WalPath", + "example path", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, loadedCfg.Mempool.WalPath) + }, + }, + { + "size updated", + []string{ + "Mempool.Size", + "100", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.Size)) + }, + }, + { + "max pending txs bytes updated", + []string{ + "Mempool.MaxPendingTxsBytes", + "100", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.MaxPendingTxsBytes)) + }, + }, + { + "cache size updated", + []string{ + "Mempool.CacheSize", + "100", + }, + func(loadedCfg *config.Config, value string) { + assert.Equal(t, value, fmt.Sprintf("%d", loadedCfg.Mempool.CacheSize)) + }, + }, + } + + verifySetTestTableCommon(t, testTable) +} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index c2632343f68..9565c4fe08f 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -9,6 +9,8 @@ import ( "github.com/peterbourgon/ff/v3/fftoml" ) +const flagConfigFlag = "flag-config-path" + func main() { cmd := newRootCmd(commands.NewDefaultIO()) @@ -21,7 +23,7 @@ func newRootCmd(io commands.IO) *commands.Command { ShortUsage: " [flags] [...]", ShortHelp: "starts the gnoland blockchain node", Options: []ff.Option{ - ff.WithConfigFileFlag("config"), + ff.WithConfigFileFlag(flagConfigFlag), ff.WithConfigFileParser(fftoml.Parser), }, }, @@ -31,6 +33,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newStartCmd(io), + newConfigCmd(io), ) return cmd diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 4b98d6afd5a..2b1757706f8 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -134,14 +134,14 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.config, - "config", + flagConfigFlag, "", "the flag config file (optional)", ) fs.StringVar( &c.nodeConfigPath, - "tm2-node-config", + "config-path", "", "the node TOML config file path (optional)", ) diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 6606d88ba2e..661d329e103 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -30,8 +30,8 @@ func TestStartInitialize(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - mockOut := bytes.NewBufferString("") - mockErr := bytes.NewBufferString("") + mockOut := new(bytes.Buffer) + mockErr := new(bytes.Buffer) io := commands.NewTestIO() io.SetOut(commands.WriteNopCloser(mockOut)) io.SetErr(commands.WriteNopCloser(mockErr)) diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 3cf6afc147b..729fafcffe5 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -12,6 +12,7 @@ import ( mem "github.com/gnolang/gno/tm2/pkg/bft/mempool/config" rpc "github.com/gnolang/gno/tm2/pkg/bft/rpc/config" eventstore "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" + "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/errors" osm "github.com/gnolang/gno/tm2/pkg/os" p2p "github.com/gnolang/gno/tm2/pkg/p2p/config" @@ -31,14 +32,8 @@ var ( ) const ( - levelDBName = "goleveldb" - clevelDBName = "cleveldb" - boltDBName = "boltdb" -) - -const ( - localABCI = "local" - socketABCI = "socket" + LocalABCI = "local" + SocketABCI = "socket" ) // Regular expression for TCP or UNIX socket address @@ -296,11 +291,11 @@ func DefaultBaseConfig() BaseConfig { NodeKey: defaultNodeKeyPath, Moniker: defaultMoniker, ProxyApp: "tcp://127.0.0.1:26658", - ABCI: "socket", + ABCI: SocketABCI, ProfListenAddress: "", FastSyncMode: true, FilterPeers: false, - DBBackend: "goleveldb", + DBBackend: db.GoLevelDBBackend.String(), DBPath: "data", } } @@ -365,9 +360,9 @@ func (cfg BaseConfig) ValidateBasic() error { } // Verify the DB backend - if cfg.DBBackend != levelDBName && - cfg.DBBackend != clevelDBName && - cfg.DBBackend != boltDBName { + if cfg.DBBackend != db.GoLevelDBBackend.String() && + cfg.DBBackend != db.CLevelDBBackend.String() && + cfg.DBBackend != db.BoltDBBackend.String() { return errInvalidDBBackend } @@ -403,8 +398,8 @@ func (cfg BaseConfig) ValidateBasic() error { } // Verify the correct ABCI mechanism is set - if cfg.ABCI != localABCI && - cfg.ABCI != socketABCI { + if cfg.ABCI != LocalABCI && + cfg.ABCI != SocketABCI { return errInvalidABCIMechanism } diff --git a/tm2/pkg/db/db.go b/tm2/pkg/db/db.go index 2c55eaf0aa6..3da7d53bc23 100644 --- a/tm2/pkg/db/db.go +++ b/tm2/pkg/db/db.go @@ -9,6 +9,10 @@ import ( type BackendType string +func (b BackendType) String() string { + return string(b) +} + // These are valid backend types. // // The backends themselves must be imported to be used (ie. using the blank