Skip to content

Commit

Permalink
Support min fees-based anti spam mechanism
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 19, 2018
1 parent c6a3928 commit 62387d9
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 46 deletions.
4 changes: 4 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 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,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)
}
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion baseapp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
66 changes: 66 additions & 0 deletions cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
13 changes: 13 additions & 0 deletions docs/getting-started/full-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ You can edit this `name` later, in the `~/.gaiad/config/config.toml` file:
moniker = "<your_custom_name>"
```

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
Expand Down
36 changes: 36 additions & 0 deletions server/config/config.go
Original file line number Diff line number Diff line change
@@ -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.
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)
}
46 changes: 46 additions & 0 deletions server/config/toml.go
Original file line number Diff line number Diff line change
@@ -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)
}
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 validator will accept for transactions")

// 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 @@ -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"
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/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
}

Expand Down
Loading

0 comments on commit 62387d9

Please sign in to comment.