diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 10d9f55f7751..fa5eb9f98b95 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -68,6 +68,9 @@ type BaseApp struct { deliverState *state // for DeliverTx signedValidators []abci.SigningValidator // absent validators from begin block + // Minimum fees for spam prevention + minimumFees sdk.Coins + // flag for sealing sealed bool } @@ -188,12 +191,12 @@ func (app *BaseApp) initFromStore(mainKey sdk.StoreKey) error { return nil } +// SetMinimumFees sets the minimum fees. +func (app *BaseApp) SetMinimumFees(fees sdk.Coins) { app.minimumFees = fees } + // NewContext returns a new Context with the correct store, the given header, and nil txBytes. func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { - if isCheckTx { - return sdk.NewContext(app.checkState.ms, header, true, app.Logger) - } - return sdk.NewContext(app.deliverState.ms, header, false, app.Logger) + return sdk.NewContext(app.checkState.ms, header, isCheckTx, app.Logger).WithMinimumFees(app.minimumFees) } type state struct { @@ -209,7 +212,7 @@ func (app *BaseApp) setCheckState(header abci.Header) { ms := app.cms.CacheMultiStore() app.checkState = &state{ ms: ms, - ctx: sdk.NewContext(ms, header, true, app.Logger), + ctx: sdk.NewContext(ms, header, true, app.Logger).WithMinimumFees(app.minimumFees), } } @@ -217,7 +220,7 @@ func (app *BaseApp) setDeliverState(header abci.Header) { ms := app.cms.CacheMultiStore() app.deliverState = &state{ ms: ms, - ctx: sdk.NewContext(ms, header, false, app.Logger), + ctx: sdk.NewContext(ms, header, false, app.Logger).WithMinimumFees(app.minimumFees), } } @@ -386,7 +389,8 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult() } - ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger) + ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger). + WithMinimumFees(app.minimumFees) // Passes the rest of the path as an argument to the querier. // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path resBytes, err := querier(ctx, path[2:], req) diff --git a/baseapp/options.go b/baseapp/options.go index 0a404217ae30..5d0cd606f256 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -26,3 +26,12 @@ func SetPruning(pruning string) func(*BaseApp) { bap.cms.SetPruning(pruningEnum) } } + +// SetMinimumFees returns an option that sets the minimum fees on the app. +func SetMinimumFees(minFees string) func(*BaseApp) { + fees, err := sdk.ParseCoins(minFees) + if err != nil { + panic(fmt.Sprintf("Invalid minimum fees: %v", err)) + } + return func(bap *BaseApp) { bap.SetMinimumFees(fees) } +} diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index aa5978407dbe..0b5f0e505e67 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -43,7 +43,10 @@ func main() { } func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { - return app.NewGaiaApp(logger, db, traceStore, baseapp.SetPruning(viper.GetString("pruning"))) + return app.NewGaiaApp(logger, db, traceStore, + baseapp.SetPruning(viper.GetString("pruning")), + baseapp.SetMinimumFees(viper.GetString("minimum_fees")), + ) } func exportAppStateAndTMValidators( diff --git a/server/config/config.go b/server/config/config.go index e6fc6a4de91c..8dea9021317e 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,5 +1,39 @@ package config +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + defaultMinimumFees = "" +) + +// BaseConfig defines the server's basic configuration +type BaseConfig struct { + // Tx minimum fee + MinFees string `mapstructure:"minimum_fees"` +} + +// Config defines the server's top level configuration +type Config struct { + BaseConfig `mapstructure:",squash"` +} + +// SetMinimumFee sets the minimum fee. +func (c *Config) SetMinimumFees(fees sdk.Coins) { c.MinFees = fees.String() } + +// SetMinimumFee sets the minimum fee. +func (c *Config) MinimumFees() sdk.Coins { + fees, err := sdk.ParseCoins(c.MinFees) + if err != nil { + panic(err) + } + return fees +} + +// DefaultConfig returns server's default configuration +func DefaultConfig() *Config { return &Config{BaseConfig{MinFees: defaultMinimumFees}} } + //_____________________________________________________________________ // Configuration structure for command functions that share configuration. diff --git a/server/config/config_test.go b/server/config/config_test.go new file mode 100644 index 000000000000..e4d552ad21f3 --- /dev/null +++ b/server/config/config_test.go @@ -0,0 +1,19 @@ +package config + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + require.True(t, cfg.MinimumFees().IsZero()) +} + +func TestSetMinimumFees(t *testing.T) { + cfg := DefaultConfig() + cfg.SetMinimumFees(sdk.Coins{sdk.NewCoin("foo", sdk.NewInt(100))}) + require.Equal(t, "100foo", cfg.MinFees) +} diff --git a/server/config/toml.go b/server/config/toml.go new file mode 100644 index 000000000000..c3542d7cfe86 --- /dev/null +++ b/server/config/toml.go @@ -0,0 +1,45 @@ +package config + +import ( + "bytes" + "text/template" + + "github.com/spf13/viper" + cmn "github.com/tendermint/tendermint/libs/common" +) + +var configTemplate *template.Template + +func init() { + var err error + if configTemplate, err = template.New("cosmosConfigFileTemplate").Parse(defaultConfigTemplate); err != nil { + panic(err) + } +} + +// ParseConfig retrieves the default environment configuration for Cosmos +func ParseConfig() (*Config, error) { + conf := DefaultConfig() + err := viper.Unmarshal(conf) + return conf, err +} + +// WriteConfigFile renders config using the template and writes it to configFilePath. +func WriteConfigFile(configFilePath string, config *Config) { + var buffer bytes.Buffer + + if err := configTemplate.Execute(&buffer, config); err != nil { + panic(err) + } + + cmn.MustWriteFile(configFilePath, buffer.Bytes(), 0644) +} + +const defaultConfigTemplate = `# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base config options ##### + +# Validators reject any tx from the mempool with less than the minimum fee per gas. +minimum_fees = "{{ .BaseConfig.MinFees }}" +` diff --git a/server/start.go b/server/start.go index 829e3936393a..1fc09533cdfa 100644 --- a/server/start.go +++ b/server/start.go @@ -19,6 +19,7 @@ const ( flagAddress = "address" flagTraceStore = "trace-store" flagPruning = "pruning" + flagMinimumFees = "minimum_fees" ) // StartCmd runs the service passed in, either stand-alone or in-process with @@ -45,6 +46,7 @@ func StartCmd(ctx *Context, appCreator AppCreator) *cobra.Command { cmd.Flags().String(flagAddress, "tcp://0.0.0.0:26658", "Listen address") cmd.Flags().String(flagTraceStore, "", "Enable KVStore tracing to an output file") cmd.Flags().String(flagPruning, "syncable", "Pruning strategy: syncable, nothing, everything") + cmd.Flags().String(flagMinimumFees, "", "Minimum fees for ante handler spam prevention") // add support for all Tendermint-specific command line options tcmd.AddNodeFlags(cmd) diff --git a/server/util.go b/server/util.go index 6aff52965128..e27a10247925 100644 --- a/server/util.go +++ b/server/util.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/viper" "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server/config" "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/wire" tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" @@ -97,6 +98,20 @@ func interceptLoadConfig() (conf *cfg.Config, err error) { if conf == nil { conf, err = tcmd.ParseConfig() } + + cosmosConfigFilePath := filepath.Join(rootDir, "config/cosmos.toml") + viper.SetConfigName("cosmos") + _ = viper.MergeInConfig() + var cosmosConf *config.Config + if _, err := os.Stat(cosmosConfigFilePath); os.IsNotExist(err) { + cosmosConf, _ := config.ParseConfig() + config.WriteConfigFile(cosmosConfigFilePath, cosmosConf) + } + + if cosmosConf == nil { + _, err = config.ParseConfig() + } + return } diff --git a/types/context.go b/types/context.go index 85fb16a7ff17..d227fb4a2741 100644 --- a/types/context.go +++ b/types/context.go @@ -41,10 +41,12 @@ func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, logger log.Lo c = c.WithBlockHeader(header) c = c.WithBlockHeight(header.Height) c = c.WithChainID(header.ChainID) + c = c.WithIsCheckTx(isCheckTx) c = c.WithTxBytes(nil) c = c.WithLogger(logger) c = c.WithSigningValidators(nil) c = c.WithGasMeter(NewInfiniteGasMeter()) + c = c.WithMinimumFees(Coins{}) return c } @@ -132,10 +134,12 @@ const ( contextKeyBlockHeight contextKeyConsensusParams contextKeyChainID + contextKeyIsCheckTx contextKeyTxBytes contextKeyLogger contextKeySigningValidators contextKeyGasMeter + contextKeyMinimumFees ) // NOTE: Do not expose MultiStore. @@ -170,6 +174,12 @@ func (c Context) SigningValidators() []abci.SigningValidator { func (c Context) GasMeter() GasMeter { return c.Value(contextKeyGasMeter).(GasMeter) } +func (c Context) IsCheckTx() bool { + return c.Value(contextKeyIsCheckTx).(bool) +} +func (c Context) MinimumFees() Coins { + return c.Value(contextKeyMinimumFees).(Coins) +} func (c Context) WithMultiStore(ms MultiStore) Context { return c.withValue(contextKeyMultiStore, ms) } @@ -202,6 +212,12 @@ func (c Context) WithSigningValidators(SigningValidators []abci.SigningValidator func (c Context) WithGasMeter(meter GasMeter) Context { return c.withValue(contextKeyGasMeter, meter) } +func (c Context) WithIsCheckTx(isCheckTx bool) Context { + return c.withValue(contextKeyIsCheckTx, isCheckTx) +} +func (c Context) WithMinimumFees(minFees Coins) Context { + return c.withValue(contextKeyMinimumFees, minFees) +} // Cache the multistore and return a new cached context. The cached context is // written to the context when writeCache is called. diff --git a/types/context_test.go b/types/context_test.go index b11a774cd90b..e08aca01ff11 100644 --- a/types/context_test.go +++ b/types/context_test.go @@ -162,20 +162,23 @@ func TestContextWithCustom(t *testing.T) { logger := NewMockLogger() signvals := []abci.SigningValidator{{}} meter := types.NewGasMeter(10000) + minFees := types.Coins{types.NewInt64Coin("feeCoin", 1)} ctx = types.NewContext(nil, header, ischeck, logger). WithBlockHeight(height). WithChainID(chainid). WithTxBytes(txbytes). WithSigningValidators(signvals). - WithGasMeter(meter) + WithGasMeter(meter). + WithMinimumFees(minFees) require.Equal(t, header, ctx.BlockHeader()) require.Equal(t, height, ctx.BlockHeight()) require.Equal(t, chainid, ctx.ChainID()) + require.Equal(t, ischeck, ctx.IsCheckTx()) require.Equal(t, txbytes, ctx.TxBytes()) require.Equal(t, logger, ctx.Logger()) require.Equal(t, signvals, ctx.SigningValidators()) require.Equal(t, meter, ctx.GasMeter()) - + require.Equal(t, minFees, types.Coins{types.NewInt64Coin("feeCoin", 1)}) } diff --git a/x/auth/ante.go b/x/auth/ante.go index 5b2cdb15599c..c18d39cfb880 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -95,8 +95,10 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { } // first sig pays the fees - // TODO: Add min fees // Can this function be moved outside of the loop? + if ctx.IsCheckTx() && !simulate && ctx.MinimumFees().Minus(fee.Amount).IsPositive() { + fee = NewStdFee(fee.Gas, ctx.MinimumFees()...) + } if i == 0 && !fee.Amount.IsZero() { newCtx.GasMeter().ConsumeGas(deductFeesCost, "deductFees") signerAcc, res = deductFees(signerAcc, fee)