From 62387d9a2878c81a501fb43b9e325259db964408 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Fri, 14 Sep 2018 10:13:40 +0100 Subject: [PATCH 1/4] Support min fees-based anti spam mechanism Create cosmos.toml configuration file and handle minimum_fees setting/flag to provide validators with a simple and flexible anti-spam mechanism. Closes: #1921 --- PENDING.md | 4 ++ baseapp/baseapp.go | 13 ++++-- baseapp/options.go | 11 ++++- cmd/gaia/cli_test/cli_test.go | 66 +++++++++++++++++++++++++++ cmd/gaia/cmd/gaiad/main.go | 5 +- docs/getting-started/full-node.md | 13 ++++++ server/config/config.go | 36 +++++++++++++++ server/config/config_test.go | 19 ++++++++ server/config/toml.go | 46 +++++++++++++++++++ server/start.go | 2 + server/util.go | 15 ++++++ types/context.go | 76 +++++++++++++++++-------------- types/context_test.go | 7 ++- types/errors.go | 6 +++ x/auth/ante.go | 29 +++++++++--- x/auth/ante_test.go | 21 +++++++++ 16 files changed, 323 insertions(+), 46 deletions(-) create mode 100644 server/config/config_test.go create mode 100644 server/config/toml.go diff --git a/PENDING.md b/PENDING.md index 75d0fe62aca..e3824a08d53 100644 --- a/PENDING.md +++ b/PENDING.md @@ -44,6 +44,7 @@ BREAKING CHANGES * [simulation] Remove log and testing.TB from Operation and Invariants, in favor of using errors \#2282 * [tools] Removed gocyclo [#2211](https://github.com/cosmos/cosmos-sdk/issues/2211) * [baseapp] Remove `SetTxDecoder` in favor of requiring the decoder be set in baseapp initialization. [#1441](https://github.com/cosmos/cosmos-sdk/issues/1441) + * [baseapp] [\#1921](https://github.com/cosmos/cosmos-sdk/issues/1921) Add minimumFees field to BaseApp. * [store] Change storeInfo within the root multistore to use tmhash instead of ripemd160 \#2308 * [codec] \#2324 All referrences to wire have been renamed to codec. Additionally, wire.NewCodec is now codec.New(). * [types] \#2343 Make sdk.Msg have a names field, to facilitate automatic tagging. @@ -75,6 +76,9 @@ FEATURES * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` + * [cli] [\#1921] (https://github.com/cosmos/cosmos-sdk/issues/1921) + * New configuration file `cosmos.config` is now created to host Gaia-specific configuration. + * New --minimum_fees/minimum_fees flag/config option to set a minimum fee. * SDK * [querier] added custom querier functionality, so ABCI query requests can be handled by keepers diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 1f994401426..63d7b17e8d0 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,10 +191,13 @@ 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.checkState.ms, header, true, app.Logger).WithMinimumFees(app.minimumFees) } return sdk.NewContext(app.deliverState.ms, header, false, app.Logger) } @@ -209,7 +215,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), } } @@ -386,7 +392,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 0a404217ae3..048a17d588f 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -20,9 +20,18 @@ func SetPruning(pruning string) func(*BaseApp) { case "syncable": pruningEnum = sdk.PruneSyncable default: - panic(fmt.Sprintf("Invalid pruning strategy: %s", pruning)) + panic(fmt.Sprintf("invalid pruning strategy: %s", pruning)) } return func(bap *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/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 3d9f86789aa..8ac8f55598a 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -36,6 +36,72 @@ func init() { gaiadHome, gaiacliHome = getTestingHomeDirs() } +func TestGaiaCLIMinimumFees(t *testing.T) { + chainID, servAddr, port := initializeFixtures(t) + flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) + + // start gaiad server with minimum fees + proc := tests.GoExecuteTWithStdout(t, fmt.Sprintf("gaiad start --home=%s --rpc.laddr=%v --minimum_fees=2feeToken", gaiadHome, servAddr)) + + defer proc.Stop(false) + tests.WaitForTMStart(port) + tests.WaitForNextNBlocksTM(2, port) + + fooAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show foo --output=json --home=%s", gaiacliHome)) + barAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show bar --output=json --home=%s", gaiacliHome)) + + fooAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf("steak").Int64()) + + success := executeWrite(t, fmt.Sprintf( + "gaiacli send %v --amount=10steak --to=%s --from=foo", flags, barAddr), app.DefaultKeyPass) + require.False(t, success) + tests.WaitForNextNBlocksTM(2, port) + +} + +func TestGaiaCLIFeesDeduction(t *testing.T) { + chainID, servAddr, port := initializeFixtures(t) + flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) + + // start gaiad server with minimum fees + proc := tests.GoExecuteTWithStdout(t, fmt.Sprintf("gaiad start --home=%s --rpc.laddr=%v --minimum_fees=1fooToken", gaiadHome, servAddr)) + + defer proc.Stop(false) + tests.WaitForTMStart(port) + tests.WaitForNextNBlocksTM(2, port) + + fooAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show foo --output=json --home=%s", gaiacliHome)) + barAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show bar --output=json --home=%s", gaiacliHome)) + + fooAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf("fooToken").Int64()) + + // test simulation + success := executeWrite(t, fmt.Sprintf( + "gaiacli send %v --amount=1000fooToken --to=%s --from=foo --fee=1fooToken --dry-run", flags, barAddr), app.DefaultKeyPass) + require.True(t, success) + tests.WaitForNextNBlocksTM(2, port) + // ensure state didn't change + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf("fooToken").Int64()) + + // insufficient funds (coins + fees) + success = executeWrite(t, fmt.Sprintf( + "gaiacli send %v --amount=1000fooToken --to=%s --from=foo --fee=1fooToken", flags, barAddr), app.DefaultKeyPass) + require.False(t, success) + tests.WaitForNextNBlocksTM(2, port) + // ensure state didn't change + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(1000), fooAcc.GetCoins().AmountOf("fooToken").Int64()) + + // test success (transfer = coins + fees) + success = executeWrite(t, fmt.Sprintf( + "gaiacli send %v --fee=300fooToken --amount=500fooToken --to=%s --from=foo", flags, barAddr), app.DefaultKeyPass) + require.True(t, success) + tests.WaitForNextNBlocksTM(2, port) +} + func TestGaiaCLISend(t *testing.T) { chainID, servAddr, port := initializeFixtures(t) flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index aa5978407db..0b5f0e505e6 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/docs/getting-started/full-node.md b/docs/getting-started/full-node.md index 87c28581b1a..66ec97cad5f 100644 --- a/docs/getting-started/full-node.md +++ b/docs/getting-started/full-node.md @@ -29,6 +29,19 @@ You can edit this `name` later, in the `~/.gaiad/config/config.toml` file: moniker = "" ``` +You can edit the `~/.gaiad/config/gaiad.toml` file in order to enable the anti spam mechanism and reject incoming transactions with less than a minimum fee: + +``` +# 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 = "" +``` + + Your full node has been initialized! Please skip to [Genesis & Seeds](#genesis-seeds). ## Upgrading From Previous Testnet diff --git a/server/config/config.go b/server/config/config.go index e6fc6a4de91..bd0d966e35b 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,5 +1,41 @@ package config +import ( + "fmt" + + 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(fmt.Sprintf("invalid minimum fees: %v", 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 00000000000..e4d552ad21f --- /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 00000000000..3c60fbdf93f --- /dev/null +++ b/server/config/toml.go @@ -0,0 +1,46 @@ +package config + +import ( + "bytes" + "text/template" + + "github.com/spf13/viper" + cmn "github.com/tendermint/tendermint/libs/common" +) + +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 }}" +` + +var configTemplate *template.Template + +func init() { + var err error + tmpl := template.New("cosmosConfigFileTemplate") + if configTemplate, err = tmpl.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) +} diff --git a/server/start.go b/server/start.go index 829e3936393..5d5b1b3eb0d 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 validator will accept for transactions") // add support for all Tendermint-specific command line options tcmd.AddNodeFlags(cmd) diff --git a/server/util.go b/server/util.go index 22dbd78678a..9508e048940 100644 --- a/server/util.go +++ b/server/util.go @@ -12,6 +12,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server/config" "github.com/cosmos/cosmos-sdk/version" tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" cfg "github.com/tendermint/tendermint/config" @@ -97,6 +98,20 @@ func interceptLoadConfig() (conf *cfg.Config, err error) { if conf == nil { conf, err = tcmd.ParseConfig() } + + cosmosConfigFilePath := filepath.Join(rootDir, "config/gaiad.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 85fb16a7ff1..6a54f247ded 100644 --- a/types/context.go +++ b/types/context.go @@ -1,3 +1,4 @@ +// nolint package types import ( @@ -41,10 +42,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 +135,12 @@ const ( contextKeyBlockHeight contextKeyConsensusParams contextKeyChainID + contextKeyIsCheckTx contextKeyTxBytes contextKeyLogger contextKeySigningValidators contextKeyGasMeter + contextKeyMinimumFees ) // NOTE: Do not expose MultiStore. @@ -145,41 +150,41 @@ func (c Context) multiStore() MultiStore { return c.Value(contextKeyMultiStore).(MultiStore) } -// nolint -func (c Context) BlockHeader() abci.Header { - return c.Value(contextKeyBlockHeader).(abci.Header) -} -func (c Context) BlockHeight() int64 { - return c.Value(contextKeyBlockHeight).(int64) -} +func (c Context) BlockHeader() abci.Header { return c.Value(contextKeyBlockHeader).(abci.Header) } + +func (c Context) BlockHeight() int64 { return c.Value(contextKeyBlockHeight).(int64) } + func (c Context) ConsensusParams() abci.ConsensusParams { return c.Value(contextKeyConsensusParams).(abci.ConsensusParams) } -func (c Context) ChainID() string { - return c.Value(contextKeyChainID).(string) -} -func (c Context) TxBytes() []byte { - return c.Value(contextKeyTxBytes).([]byte) -} -func (c Context) Logger() log.Logger { - return c.Value(contextKeyLogger).(log.Logger) -} + +func (c Context) ChainID() string { return c.Value(contextKeyChainID).(string) } + +func (c Context) TxBytes() []byte { return c.Value(contextKeyTxBytes).([]byte) } + +func (c Context) Logger() log.Logger { return c.Value(contextKeyLogger).(log.Logger) } + func (c Context) SigningValidators() []abci.SigningValidator { return c.Value(contextKeySigningValidators).([]abci.SigningValidator) } -func (c Context) GasMeter() GasMeter { - return c.Value(contextKeyGasMeter).(GasMeter) -} -func (c Context) WithMultiStore(ms MultiStore) Context { - return c.withValue(contextKeyMultiStore, ms) -} + +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) } + func (c Context) WithBlockHeader(header abci.Header) Context { var _ proto.Message = &header // for cloning. return c.withValue(contextKeyBlockHeader, header) } + func (c Context) WithBlockHeight(height int64) Context { return c.withValue(contextKeyBlockHeight, height) } + func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context { if params == nil { return c @@ -187,20 +192,25 @@ func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context { return c.withValue(contextKeyConsensusParams, params). WithGasMeter(NewGasMeter(params.TxSize.MaxGas)) } -func (c Context) WithChainID(chainID string) Context { - return c.withValue(contextKeyChainID, chainID) -} -func (c Context) WithTxBytes(txBytes []byte) Context { - return c.withValue(contextKeyTxBytes, txBytes) -} -func (c Context) WithLogger(logger log.Logger) Context { - return c.withValue(contextKeyLogger, logger) -} + +func (c Context) WithChainID(chainID string) Context { return c.withValue(contextKeyChainID, chainID) } + +func (c Context) WithTxBytes(txBytes []byte) Context { return c.withValue(contextKeyTxBytes, txBytes) } + +func (c Context) WithLogger(logger log.Logger) Context { return c.withValue(contextKeyLogger, logger) } + func (c Context) WithSigningValidators(SigningValidators []abci.SigningValidator) Context { return c.withValue(contextKeySigningValidators, SigningValidators) } -func (c Context) WithGasMeter(meter GasMeter) Context { - return c.withValue(contextKeyGasMeter, meter) + +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 diff --git a/types/context_test.go b/types/context_test.go index b11a774cd90..e08aca01ff1 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/types/errors.go b/types/errors.go index 1d0de5eb4b4..1d4900d3c24 100644 --- a/types/errors.go +++ b/types/errors.go @@ -56,6 +56,7 @@ const ( CodeInvalidCoins CodeType = 11 CodeOutOfGas CodeType = 12 CodeMemoTooLarge CodeType = 13 + CodeInsufficientFee CodeType = 14 // CodespaceRoot is a codespace for error codes in this file only. // Notice that 0 is an "unset" codespace, which can be overridden with @@ -101,6 +102,8 @@ func CodeToDefaultMsg(code CodeType) string { return "out of gas" case CodeMemoTooLarge: return "memo too large" + case CodeInsufficientFee: + return "insufficient fee" default: return unknownCodeMsg(code) } @@ -150,6 +153,9 @@ func ErrOutOfGas(msg string) Error { func ErrMemoTooLarge(msg string) Error { return newErrorWithRootCodespace(CodeMemoTooLarge, msg) } +func ErrInsufficientFee(msg string) Error { + return newErrorWithRootCodespace(CodeInsufficientFee, msg) +} //---------------------------------------- // Error & sdkError diff --git a/x/auth/ante.go b/x/auth/ante.go index 5b2cdb15599..13dce394726 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -12,11 +12,12 @@ import ( ) const ( - deductFeesCost sdk.Gas = 10 - memoCostPerByte sdk.Gas = 1 - ed25519VerifyCost = 59 - secp256k1VerifyCost = 100 - maxMemoCharacters = 100 + deductFeesCost sdk.Gas = 10 + memoCostPerByte sdk.Gas = 1 + ed25519VerifyCost = 59 + secp256k1VerifyCost = 100 + maxMemoCharacters = 100 + feeDeductionGasFactor = 0.001 ) // NewAnteHandler returns an AnteHandler that checks @@ -94,8 +95,15 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { return newCtx, res, true } + if requiredFees := adjustFeesByGas(ctx.MinimumFees(), fee.Gas); ctx.IsCheckTx() && !simulate && + // fees must be greater than the minimum set by the validator adjusted by gas + !ctx.MinimumFees().IsZero() && !fee.Amount.IsGTE(requiredFees) { + // validators reject any tx from the mempool with less than the minimum fee per gas * gas factor + return newCtx, sdk.ErrInsufficientFee(fmt.Sprintf( + "insufficient fee, got: %q required: %q", fee.Amount, requiredFees)).Result(), true + } + // first sig pays the fees - // TODO: Add min fees // Can this function be moved outside of the loop? if i == 0 && !fee.Amount.IsZero() { newCtx.GasMeter().ConsumeGas(deductFeesCost, "deductFees") @@ -236,6 +244,15 @@ func consumeSignatureVerificationGas(meter sdk.GasMeter, pubkey crypto.PubKey) { } } +func adjustFeesByGas(fees sdk.Coins, gas int64) sdk.Coins { + gasCost := int64(float64(gas) * feeDeductionGasFactor) + gasFees := make(sdk.Coins, len(fees)) + for i := 0; i < len(fees); i++ { + gasFees[i] = sdk.NewInt64Coin(fees[i].Denom, gasCost) + } + return fees.Plus(gasFees) +} + // Deduct the fee from the account. // We could use the CoinKeeper (in addition to the AccountMapper, // because the CoinKeeper doesn't give us accounts), but it seems easier to do this. diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go index 75fd130436d..2a289f317bf 100644 --- a/x/auth/ante_test.go +++ b/x/auth/ante_test.go @@ -627,3 +627,24 @@ func TestConsumeSignatureVerificationGas(t *testing.T) { }) } } + +func TestAdjustFeesByGas(t *testing.T) { + type args struct { + fee sdk.Coins + gas int64 + } + tests := []struct { + name string + args args + want sdk.Coins + }{ + {"nil coins", args{sdk.Coins{}, 10000}, sdk.Coins{}}, + {"nil coins", args{sdk.Coins{sdk.NewInt64Coin("A", 10), sdk.NewInt64Coin("B", 0)}, 10000}, sdk.Coins{sdk.NewInt64Coin("A", 20), sdk.NewInt64Coin("B", 10)}}, + {"negative coins", args{sdk.Coins{sdk.NewInt64Coin("A", -10), sdk.NewInt64Coin("B", 10)}, 10000}, sdk.Coins{sdk.NewInt64Coin("B", 20)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.True(t, tt.want.IsEqual(adjustFeesByGas(tt.args.fee, tt.args.gas))) + }) + } +} From 2ff20721827ccf6405bef5f52219b7a2a518f31f Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Wed, 19 Sep 2018 11:22:50 +0100 Subject: [PATCH 2/4] Amend PENDING.md --- PENDING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PENDING.md b/PENDING.md index e3824a08d53..cf1dcd8995c 100644 --- a/PENDING.md +++ b/PENDING.md @@ -77,7 +77,7 @@ FEATURES * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` * [cli] [\#1921] (https://github.com/cosmos/cosmos-sdk/issues/1921) - * New configuration file `cosmos.config` is now created to host Gaia-specific configuration. + * New configuration file `gaiad.toml` is now created to host Gaia-specific configuration. * New --minimum_fees/minimum_fees flag/config option to set a minimum fee. * SDK From 04c6e6a5fb49d82ed5aedbc4878e9225e14765b6 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Wed, 19 Sep 2018 14:04:15 +0100 Subject: [PATCH 3/4] ACK bez comment --- x/auth/ante.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/auth/ante.go b/x/auth/ante.go index 13dce394726..d60868d90b4 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -95,9 +95,9 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { return newCtx, res, true } - if requiredFees := adjustFeesByGas(ctx.MinimumFees(), fee.Gas); ctx.IsCheckTx() && !simulate && - // fees must be greater than the minimum set by the validator adjusted by gas - !ctx.MinimumFees().IsZero() && !fee.Amount.IsGTE(requiredFees) { + requiredFees := adjustFeesByGas(ctx.MinimumFees(), fee.Gas) + // fees must be greater than the minimum set by the validator adjusted by gas + if ctx.IsCheckTx() && !simulate && !ctx.MinimumFees().IsZero() && !fee.Amount.IsGTE(requiredFees) { // validators reject any tx from the mempool with less than the minimum fee per gas * gas factor return newCtx, sdk.ErrInsufficientFee(fmt.Sprintf( "insufficient fee, got: %q required: %q", fee.Amount, requiredFees)).Result(), true From 1cdf1b7d63b49fa9da27b1568c2bf225b9057e61 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Wed, 19 Sep 2018 14:27:07 +0100 Subject: [PATCH 4/4] Implement types.Coin{,s}.IsLT() --- types/coin.go | 12 ++++++++++++ types/coin_test.go | 20 ++++++++++++++++++++ x/auth/ante.go | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/types/coin.go b/types/coin.go index aa602955904..d7484a66997 100644 --- a/types/coin.go +++ b/types/coin.go @@ -46,6 +46,12 @@ func (coin Coin) IsGTE(other Coin) bool { return coin.SameDenomAs(other) && (!coin.Amount.LT(other.Amount)) } +// IsLT returns true if they are the same type and the receiver is +// a smaller value +func (coin Coin) IsLT(other Coin) bool { + return !coin.IsGTE(other) +} + // IsEqual returns true if the two sets of Coins have the same value func (coin Coin) IsEqual(other Coin) bool { return coin.SameDenomAs(other) && (coin.Amount.Equal(other.Amount)) @@ -181,6 +187,12 @@ func (coins Coins) IsGTE(coinsB Coins) bool { return diff.IsNotNegative() } +// IsLT returns True iff every currency in coins, the currency is +// present at a smaller amount in coins +func (coins Coins) IsLT(coinsB Coins) bool { + return !coins.IsGTE(coinsB) +} + // IsZero returns true if there are no coins // or all coins are zero. func (coins Coins) IsZero() bool { diff --git a/types/coin_test.go b/types/coin_test.go index 145c0c40a2c..3ff0bffe571 100644 --- a/types/coin_test.go +++ b/types/coin_test.go @@ -76,6 +76,24 @@ func TestIsGTECoin(t *testing.T) { } } +func TestIsLTCoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + }{ + {NewInt64Coin("A", 1), NewInt64Coin("A", 1), false}, + {NewInt64Coin("A", 2), NewInt64Coin("A", 1), false}, + {NewInt64Coin("A", -1), NewInt64Coin("A", 5), true}, + {NewInt64Coin("a", 0), NewInt64Coin("b", 1), true}, + } + + for tcIndex, tc := range cases { + res := tc.inputOne.IsLT(tc.inputTwo) + require.Equal(t, tc.expected, res, "coin LT relation is incorrect, tc #%d", tcIndex) + } +} + func TestIsEqualCoin(t *testing.T) { cases := []struct { inputOne Coin @@ -227,6 +245,8 @@ func TestCoins(t *testing.T) { assert.True(t, good.IsPositive(), "Expected coins to be positive: %v", good) assert.False(t, null.IsPositive(), "Expected coins to not be positive: %v", null) assert.True(t, good.IsGTE(empty), "Expected %v to be >= %v", good, empty) + assert.False(t, good.IsLT(empty), "Expected %v to be < %v", good, empty) + assert.True(t, empty.IsLT(good), "Expected %v to be < %v", empty, good) assert.False(t, neg.IsPositive(), "Expected neg coins to not be positive: %v", neg) assert.Zero(t, len(sum), "Expected 0 coins") assert.False(t, badSort1.IsValid(), "Coins are not sorted") diff --git a/x/auth/ante.go b/x/auth/ante.go index d60868d90b4..5773e6b1fe6 100644 --- a/x/auth/ante.go +++ b/x/auth/ante.go @@ -97,7 +97,7 @@ func NewAnteHandler(am AccountMapper, fck FeeCollectionKeeper) sdk.AnteHandler { requiredFees := adjustFeesByGas(ctx.MinimumFees(), fee.Gas) // fees must be greater than the minimum set by the validator adjusted by gas - if ctx.IsCheckTx() && !simulate && !ctx.MinimumFees().IsZero() && !fee.Amount.IsGTE(requiredFees) { + if ctx.IsCheckTx() && !simulate && !ctx.MinimumFees().IsZero() && fee.Amount.IsLT(requiredFees) { // validators reject any tx from the mempool with less than the minimum fee per gas * gas factor return newCtx, sdk.ErrInsufficientFee(fmt.Sprintf( "insufficient fee, got: %q required: %q", fee.Amount, requiredFees)).Result(), true