diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5074bbfc1a..8f77082115bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [\#9205](https://github.com/cosmos/cosmos-sdk/pull/9205) Improve readability in `abci` handleQueryP2P * [\#9314](https://github.com/cosmos/cosmos-sdk/pull/9314) Update Rosetta SDK to upstream's latest release. + ### Features * [\#8077](https://github.com/cosmos/cosmos-sdk/pull/8077) Added support for grpc-web, enabling browsers to communicate with a chain's gRPC server @@ -127,6 +128,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (store) [\#8790](https://github.com/cosmos/cosmos-sdk/pull/8790) Reduce gas costs by 10x for transient store operations. * (x/bank) [\#9051](https://github.com/cosmos/cosmos-sdk/pull/9051) Supply value is stored as `sdk.Int` rather than `string`. + +### CLI Breaking Changes + +* [\#9371](https://github.com/cosmos/cosmos-sdk/pull/9371) Non-zero default fees/Server will error if there's an empty value for min-gas-price in app.toml + + ### Improvements * (baseapp) [\#9578](https://github.com/cosmos/cosmos-sdk/pull/9578) Return `Baseapp`'s `trace` value for logging error stack traces. diff --git a/contrib/rosetta/node/data.tar.gz b/contrib/rosetta/node/data.tar.gz index 3c37adcc162f..81ad29831d2b 100644 Binary files a/contrib/rosetta/node/data.tar.gz and b/contrib/rosetta/node/data.tar.gz differ diff --git a/docs/core/cli.md b/docs/core/cli.md index 5fbb82f0ecb0..36a225cb3ad5 100644 --- a/docs/core/cli.md +++ b/docs/core/cli.md @@ -64,10 +64,14 @@ Next is an example `rootCmd` function from the `simapp` application. It instanti `rootCmd` has a function called `initAppConfig()` which is useful for setting the application's custom configs. By default app uses Tendermint app config template from SDK, which can be over-written via `initAppConfig()`. -Here's an example code to override default `appConfig` and `app.toml` template. +Here's an example code to override default `app.toml` template. +++ https://github.com/cosmos/cosmos-sdk/blob/4eea4cafd3b8b1c2cd493886db524500c9dd745c/simapp/simd/cmd/root.go#L84-L117 +The `initAppConfig()` also allows overriding the default SDK's [server config](https://github.com/cosmos/cosmos-sdk/blob/4eea4cafd3b8b1c2cd493886db524500c9dd745c/server/config/config.go#L199). One example is the `min-gas-prices` config, which defines the minimum gas prices a validator is willing to accept for processing a transaction. By default, the SDK sets this parameter to `""` (empty string), which forces all validators to tweak their own `app.toml` and set a non-empty value, or else the node will halt on startup. This might not be the best UX for validators, so the chain developer can set a default `app.toml` value for validators inside this `initAppConfig()` function. + ++++ https://github.com/cosmos/cosmos-sdk/blob/aa9b055ddb46aacd4737335a92d0b8a82d577341/simapp/simd/cmd/root.go#L101-L116 + The root-level `status` and `keys` subcommands are common across most applications and do not interact with application state. The bulk of an application's functionality - what users can actually _do_ with it - is enabled by its `tx` and `query` commands. ### Transaction Commands diff --git a/docs/run-node/run-node.md b/docs/run-node/run-node.md index d7aaf8f5822d..119dca558426 100644 --- a/docs/run-node/run-node.md +++ b/docs/run-node/run-node.md @@ -71,6 +71,24 @@ For more information on `gentx`, use the following command: simd gentx --help ``` +## Configuring the Node Using `app.toml` and `config.toml` + +The Cosmos SDK automatically generates two configuration files inside `~/.simapp/config`: + +- `config.toml`: used to configure the Tendermint, learn more on [Tendermint's documentation](https://docs.tendermint.com/master/nodes/configuration.html), +- `app.toml`: generated by the Cosmos SDK, and used to configure your app, such as state pruning strategies, telemetry, gRPC and REST servers configuration, state sync... + +Both files are heavily commented, please refer to them directly to tweak your node. + +One example config to tweak is the `minimum-gas-prices` field inside `app.toml`, which defines the minimum gas prices the validator node is willing to accept for processing a transaction. Depending on the chain, it might be an empty string or not. If it's empty, make sure to edit the field with some value, for example `10token`, or else the node will halt on startup. For the purpose of this tutorial, let's set the minimum gas price to 0: + +```toml + # The minimum gas prices a validator is willing to accept for processing a + # transaction. A transaction's fees must meet the minimum of any denomination + # specified in this config (e.g. 0.25token1;0.0001token2). + minimum-gas-prices = "0stake" +``` + ## Run a Localnet Now that everything is set up, you can finally start your node: @@ -85,12 +103,6 @@ The previous command allow you to run a single node. This is enough for the next The naive way would be to run the same commands again in separate terminal windows. This is possible, however in the SDK, we leverage the power of [Docker Compose](https://docs.docker.com/compose/) to run a localnet. If you need inspiration on how to set up your own localnet with Docker Compose, you can have a look at the SDK's [`docker-compose.yml`](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0-rc3/docker-compose.yml). -## Configuring the Node Using `app.toml` - -The Cosmos SDK automatically generates an `app.toml` file inside `~/.simapp/config`. This file is used to configure your app, such as state pruning strategies, telemetry, gRPC and REST servers configuration, state sync... The file itself is heavily commented, please refer to it directly to tweak your node. - -Make sure to restart your node after modifying `app.toml`. - ## Next {hide} Read about the [Interacting with your Node](./interact-node.md) {hide} diff --git a/server/config/config.go b/server/config/config.go index 907e64a63490..b3d8a99cf6fe 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -9,6 +9,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) const ( @@ -310,3 +311,12 @@ func GetConfig(v *viper.Viper) Config { }, } } + +// ValidateBasic returns an error if min-gas-prices field is empty in BaseConfig. Otherwise, it returns nil. +func (c Config) ValidateBasic() error { + if c.BaseConfig.MinGasPrices == "" { + return sdkerrors.ErrAppConfig.Wrap("set min gas price in app.toml or flag or env variable") + } + + return nil +} diff --git a/server/start.go b/server/start.go index 0c084c858ec2..8257f5d8f594 100644 --- a/server/start.go +++ b/server/start.go @@ -245,6 +245,11 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App return err } + config := config.GetConfig(ctx.Viper) + if err := config.ValidateBasic(); err != nil { + return err + } + app := appCreator(ctx.Logger, db, traceWriter, ctx.Viper) nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) @@ -273,8 +278,6 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App } ctx.Logger.Debug("initialization: tmNode started") - config := config.GetConfig(ctx.Viper) - // Add the tx service to the gRPC router. We only need to register this // service if API or gRPC is enabled, and avoid doing so in the general // case, because it spawns a new local tendermint RPC client. diff --git a/server/util_test.go b/server/util_test.go index d5674be1a681..b83531f562e5 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -1,4 +1,4 @@ -package server +package server_test import ( "context" @@ -11,35 +11,42 @@ import ( "testing" "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/config" + "github.com/cosmos/cosmos-sdk/simapp" + genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) -var CancelledInPreRun = errors.New("Canelled in prerun") +var cancelledInPreRun = errors.New("Cancelled in prerun") // Used in each test to run the function under test via Cobra // but to always halt the command func preRunETestImpl(cmd *cobra.Command, args []string) error { - err := InterceptConfigsPreRunHandler(cmd, "", nil) + err := server.InterceptConfigsPreRunHandler(cmd, "", nil) if err != nil { return err } - return CancelledInPreRun + return cancelledInPreRun } func TestInterceptConfigsPreRunHandlerCreatesConfigFilesWhenMissing(t *testing.T) { tempDir := t.TempDir() - cmd := StartCmd(nil, "/foobar") + cmd := server.StartCmd(nil, "/foobar") if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil { t.Fatalf("Could not set home flag [%T] %v", err, err) } cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) - if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun { + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) + if err := cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -106,17 +113,17 @@ func TestInterceptConfigsPreRunHandlerReadsConfigToml(t *testing.T) { t.Fatalf("Failed closing config.toml: %v", err) } - cmd := StartCmd(nil, "/foobar") + cmd := server.StartCmd(nil, "/foobar") if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil { t.Fatalf("Could not set home flag [%T] %v", err, err) } cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -146,14 +153,14 @@ func TestInterceptConfigsPreRunHandlerReadsAppToml(t *testing.T) { if err := writer.Close(); err != nil { t.Fatalf("Failed closing app.toml: %v", err) } - cmd := StartCmd(nil, tempDir) + cmd := server.StartCmd(nil, tempDir) cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -165,7 +172,7 @@ func TestInterceptConfigsPreRunHandlerReadsAppToml(t *testing.T) { func TestInterceptConfigsPreRunHandlerReadsFlags(t *testing.T) { const testAddr = "tcp://127.1.2.3:12345" tempDir := t.TempDir() - cmd := StartCmd(nil, "/foobar") + cmd := server.StartCmd(nil, "/foobar") if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil { t.Fatalf("Could not set home flag [%T] %v", err, err) @@ -178,10 +185,10 @@ func TestInterceptConfigsPreRunHandlerReadsFlags(t *testing.T) { cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -193,7 +200,7 @@ func TestInterceptConfigsPreRunHandlerReadsFlags(t *testing.T) { func TestInterceptConfigsPreRunHandlerReadsEnvVars(t *testing.T) { const testAddr = "tcp://127.1.2.3:12345" tempDir := t.TempDir() - cmd := StartCmd(nil, "/foobar") + cmd := server.StartCmd(nil, "/foobar") if err := cmd.Flags().Set(flags.FlagHome, tempDir); err != nil { t.Fatalf("Could not set home flag [%T] %v", err, err) } @@ -213,10 +220,10 @@ func TestInterceptConfigsPreRunHandlerReadsEnvVars(t *testing.T) { cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -279,7 +286,7 @@ func newPrecedenceCommon(t *testing.T) precedenceCommon { }) // Set up the command object that is used in this test - retval.cmd = StartCmd(nil, tempDir) + retval.cmd = server.StartCmd(nil, tempDir) retval.cmd.PreRunE = preRunETestImpl return retval @@ -317,10 +324,10 @@ func TestInterceptConfigsPreRunHandlerPrecedenceFlag(t *testing.T) { testCommon := newPrecedenceCommon(t) testCommon.setAll(t, &TestAddrExpected, &TestAddrNotExpected, &TestAddrNotExpected) - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := testCommon.cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -333,10 +340,10 @@ func TestInterceptConfigsPreRunHandlerPrecedenceEnvVar(t *testing.T) { testCommon := newPrecedenceCommon(t) testCommon.setAll(t, nil, &TestAddrExpected, &TestAddrNotExpected) - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := testCommon.cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -349,10 +356,10 @@ func TestInterceptConfigsPreRunHandlerPrecedenceConfigFile(t *testing.T) { testCommon := newPrecedenceCommon(t) testCommon.setAll(t, nil, nil, &TestAddrExpected) - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := testCommon.cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -365,10 +372,10 @@ func TestInterceptConfigsPreRunHandlerPrecedenceConfigDefault(t *testing.T) { testCommon := newPrecedenceCommon(t) // Do not set anything - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) - if err := testCommon.cmd.ExecuteContext(ctx); err != CancelledInPreRun { + if err := testCommon.cmd.ExecuteContext(ctx); err != cancelledInPreRun { t.Fatalf("function failed with [%T] %v", err, err) } @@ -386,16 +393,47 @@ func TestInterceptConfigsWithBadPermissions(t *testing.T) { if err := os.Mkdir(subDir, 0600); err != nil { t.Fatalf("Failed to create sub directory: %v", err) } - cmd := StartCmd(nil, "/foobar") + cmd := server.StartCmd(nil, "/foobar") if err := cmd.Flags().Set(flags.FlagHome, subDir); err != nil { t.Fatalf("Could not set home flag [%T] %v", err, err) } cmd.PreRunE = preRunETestImpl - serverCtx := &Context{} - ctx := context.WithValue(context.Background(), ServerContextKey, serverCtx) + serverCtx := &server.Context{} + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) if err := cmd.ExecuteContext(ctx); !os.IsPermission(err) { t.Fatalf("Failed to catch permissions error, got: [%T] %v", err, err) } } + +func TestEmptyMinGasPrices(t *testing.T) { + tempDir := t.TempDir() + err := os.Mkdir(filepath.Join(tempDir, "config"), os.ModePerm) + require.NoError(t, err) + encCfg := simapp.MakeTestEncodingConfig() + + // Run InitCmd to create necessary config files. + clientCtx := client.Context{}.WithHomeDir(tempDir).WithJSONCodec(encCfg.Marshaler) + serverCtx := server.NewDefaultContext() + ctx := context.WithValue(context.Background(), server.ServerContextKey, serverCtx) + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + cmd := genutilcli.InitCmd(simapp.ModuleBasics, tempDir) + cmd.SetArgs([]string{"appnode-test"}) + err = cmd.ExecuteContext(ctx) + require.NoError(t, err) + + // Modify app.toml. + appCfgTempFilePath := filepath.Join(tempDir, "config", "app.toml") + appConf := config.DefaultConfig() + appConf.BaseConfig.MinGasPrices = "" + config.WriteConfigFile(appCfgTempFilePath, appConf) + + // Run StartCmd. + cmd = server.StartCmd(nil, tempDir) + cmd.PreRunE = func(cmd *cobra.Command, _ []string) error { + return server.InterceptConfigsPreRunHandler(cmd, "", nil) + } + err = cmd.ExecuteContext(ctx) + require.Errorf(t, err, sdkerrors.ErrAppConfig.Error()) +} diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index a6cbda031419..b33a660c87e3 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -98,8 +98,25 @@ func initAppConfig() (string, interface{}) { WASM WASMConfig `mapstructure:"wasm"` } + // Optionally allow the chain developer to overwrite the SDK's default + // server config. + srvCfg := serverconfig.DefaultConfig() + // The SDK's default minimum gas price is set to "" (empty value) inside + // app.toml. If left empty by validators, the node will halt on startup. + // However, the chain developer can set a default app.toml value for their + // validators here. + // + // In summary: + // - if you leave srvCfg.MinGasPrices = "", all validators MUST tweak their + // own app.toml config, + // - if you set srvCfg.MinGasPrices non-empty, validators CAN tweak their + // own app.toml to override, or use this default value. + // + // In simapp, we set the min gas prices to 0. + srvCfg.MinGasPrices = "0stake" + customAppConfig := CustomAppConfig{ - Config: *serverconfig.DefaultConfig(), + Config: *srvCfg, WASM: WASMConfig{ LruSize: 1, QueryGasLimit: 300000, @@ -207,7 +224,7 @@ type appCreator struct { encCfg params.EncodingConfig } -// newApp is an AppCreator +// newApp is an appCreator func (a appCreator) newApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application { var cache sdk.MultiStorePersistentCache diff --git a/testutil/network/util.go b/testutil/network/util.go index 126ca7e81c42..c6a2bd815904 100644 --- a/testutil/network/util.go +++ b/testutil/network/util.go @@ -27,6 +27,10 @@ func startInProcess(cfg Config, val *Validator) error { tmCfg := val.Ctx.Config tmCfg.Instrumentation.Prometheus = false + if err := val.AppConfig.ValidateBasic(); err != nil { + return err + } + nodeKey, err := p2p.LoadOrGenNodeKey(tmCfg.NodeKeyFile()) if err != nil { return err diff --git a/types/errors/errors.go b/types/errors/errors.go index 9127b985220f..62dab18adf8c 100644 --- a/types/errors/errors.go +++ b/types/errors/errors.go @@ -144,6 +144,9 @@ var ( // ErrPanic is only set when we recover from a panic, so we know to // redact potentially sensitive system info ErrPanic = Register(UndefinedCodespace, 111222, "panic") + + // ErrAppConfig defines an error occurred if min-gas-prices field in BaseConfig is empty. + ErrAppConfig = Register(RootCodespace, 40, "error in app.toml") ) // Register returns an error instance that should be used as the base for