Skip to content

Commit

Permalink
Support min fees-based anti spam strategy
Browse files Browse the repository at this point in the history
Create cosmos.toml configuration file and handle minimum_fees
setting/flag to provide validators with a simple and flexible
anti-spam mechanism.

Closes: #1921
  • Loading branch information
Alessio Treglia committed Sep 13, 2018
1 parent 5bf9401 commit e5edb2a
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 11 deletions.
18 changes: 11 additions & 7 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -209,15 +212,15 @@ 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),
}
}

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),
}
}

Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions baseapp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
5 changes: 4 additions & 1 deletion cmd/gaia/cmd/gaiad/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
34 changes: 34 additions & 0 deletions server/config/config.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
19 changes: 19 additions & 0 deletions server/config/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
45 changes: 45 additions & 0 deletions server/config/toml.go
Original file line number Diff line number Diff line change
@@ -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 }}"
`
2 changes: 2 additions & 0 deletions server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
16 changes: 16 additions & 0 deletions types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -132,10 +134,12 @@ const (
contextKeyBlockHeight
contextKeyConsensusParams
contextKeyChainID
contextKeyIsCheckTx
contextKeyTxBytes
contextKeyLogger
contextKeySigningValidators
contextKeyGasMeter
contextKeyMinimumFees
)

// NOTE: Do not expose MultiStore.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions types/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
}
4 changes: 3 additions & 1 deletion x/auth/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit e5edb2a

Please sign in to comment.