Merge PR #2328: Support min fees-based anti spam strategy

This commit is contained in:
Alessio Treglia 2018-09-19 16:25:52 +01:00 committed by Christopher Goes
parent c6a3928d37
commit b74a6a9066
18 changed files with 355 additions and 46 deletions

View File

@ -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 `gaiad.toml` 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

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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(

View File

@ -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

View File

@ -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.

View File

@ -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
server/config/toml.go Normal file
View File

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

View File

@ -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)

View File

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

View File

@ -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 {

View File

@ -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")

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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