Skip to content

Commit

Permalink
[Feature] lower oracle feeder cost (#463)
Browse files Browse the repository at this point in the history
* add tx-gas-hard-limit flag to prevent spamming attack

* convert spamming protection to mempool operation

* remove oracle messages from gas charging

* prevent huge gas

* add mempool logic to check oracle spamming attack

* remove unused app function

* update test

* prevent simulation to change map

* make bondcheck default after softfork

* update simulation

* update simulation
  • Loading branch information
yys authored Mar 18, 2021
1 parent 8545958 commit 534de88
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 61 deletions.
7 changes: 6 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,12 @@ func NewTerraApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest
// initialize BaseApp
app.SetInitChainer(app.InitChainer)
app.SetBeginBlocker(app.BeginBlocker)
app.SetAnteHandler(ante.NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.treasuryKeeper, auth.DefaultSigVerificationGasConsumer))
app.SetAnteHandler(ante.NewAnteHandler(
app.accountKeeper,
app.supplyKeeper,
app.oracleKeeper,
app.treasuryKeeper,
auth.DefaultSigVerificationGasConsumer))
app.SetEndBlocker(app.EndBlocker)

if loadLatest {
Expand Down
13 changes: 9 additions & 4 deletions x/auth/ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import (
// NewAnteHandler returns an AnteHandler that checks and increments sequence
// numbers, checks signatures & account numbers, and deducts fees from the first
// signer.
func NewAnteHandler(ak keeper.AccountKeeper, supplyKeeper types.SupplyKeeper, treasuryKeeper TreasuryKeeper, sigGasConsumer cosmosante.SignatureVerificationGasConsumer) sdk.AnteHandler {
func NewAnteHandler(
ak keeper.AccountKeeper,
supplyKeeper types.SupplyKeeper,
oracleKeeper OracleKeeper,
treasuryKeeper TreasuryKeeper,
sigGasConsumer cosmosante.SignatureVerificationGasConsumer) sdk.AnteHandler {
return sdk.ChainAnteDecorators(
cosmosante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
NewSpammingPreventionDecorator(),
NewTaxFeeDecorator(treasuryKeeper), // mempool gas fee validation & record tax proceeds
cosmosante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
NewSpammingPreventionDecorator(oracleKeeper), // spamming prevention
NewTaxFeeDecorator(treasuryKeeper), // mempool gas fee validation & record tax proceeds
cosmosante.NewValidateBasicDecorator(),
cosmosante.NewValidateMemoDecorator(ak),
cosmosante.NewConsumeGasForTxSizeDecorator(ak),
Expand Down
7 changes: 6 additions & 1 deletion x/auth/ante/expected_keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// Treasurykeeper for tax charging & recording
// TreasuryKeeper for tax charging & recording
type TreasuryKeeper interface {
RecordEpochTaxProceeds(ctx sdk.Context, delta sdk.Coins)
GetTaxRate(ctx sdk.Context) (taxRate sdk.Dec)
GetTaxCap(ctx sdk.Context, denom string) (taxCap sdk.Int)
}

// OracleKeeper for feeder validation
type OracleKeeper interface {
ValidateFeeder(ctx sdk.Context, feederAddr sdk.AccAddress, validatorAddr sdk.ValAddress, checkBonded bool) error
}
73 changes: 70 additions & 3 deletions x/auth/ante/spamming_prevention.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package ante

import (
"sync"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/spf13/viper"

core "github.com/terra-project/core/types"
oracleexported "github.com/terra-project/core/x/oracle/exported"
)

// FlagTxGasHardLimit defines the hard cap to prevent tx spamming attack
Expand All @@ -15,11 +18,20 @@ const transactionGasHardCap = 30000000
// SpammingPreventionDecorator will check if the transaction's gas is smaller than
// configured hard cap
type SpammingPreventionDecorator struct {
oracleKeeper OracleKeeper
oraclePrevoteMap map[string]int64
oracleVoteMap map[string]int64
mu *sync.Mutex
}

// NewSpammingPreventionDecorator returns new spamming prevention decorator instance
func NewSpammingPreventionDecorator() SpammingPreventionDecorator {
return SpammingPreventionDecorator{}
func NewSpammingPreventionDecorator(oracleKeeper OracleKeeper) SpammingPreventionDecorator {
return SpammingPreventionDecorator{
oracleKeeper: oracleKeeper,
oraclePrevoteMap: make(map[string]int64),
oracleVoteMap: make(map[string]int64),
mu: &sync.Mutex{},
}
}

// AnteHandle handles msg tax fee checking
Expand All @@ -33,7 +45,20 @@ func (spd SpammingPreventionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, si
if ctx.IsCheckTx() {
gasHardLimit := viper.GetUint64(FlagTxGasHardLimit)
if gas > gasHardLimit {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrOutOfGas, "Tx cannot spend more than %d gas", gasHardLimit)
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "Tx cannot spend more than %d gas", gasHardLimit)
}

if !simulate {
err := spd.CheckOracleSpamming(ctx, feeTx.GetMsgs())
if err != nil {
return ctx, err
}
}
}

if !core.IsWaitingForSoftfork(ctx, 2) {
if gas > transactionGasHardCap {
return ctx, sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "Tx exceed max allowed gas usage")
}
}

Expand All @@ -45,3 +70,45 @@ func (spd SpammingPreventionDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, si

return next(ctx, tx, simulate)
}

// CheckOracleSpamming check whether the msgs are spamming purpose or not
func (spd SpammingPreventionDecorator) CheckOracleSpamming(ctx sdk.Context, msgs []sdk.Msg) error {
spd.mu.Lock()
defer spd.mu.Unlock()

curHeight := ctx.BlockHeight()
for _, msg := range msgs {
switch msg := msg.(type) {
case oracleexported.MsgAggregateExchangeRatePrevote:
err := spd.oracleKeeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, true)
if err != nil {
return err
}

valAddrStr := msg.Validator.String()
if lastSubmittedHeight, ok := spd.oraclePrevoteMap[valAddrStr]; ok && lastSubmittedHeight == curHeight {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "the validator has already been submitted prevote at the current height")
}

spd.oraclePrevoteMap[valAddrStr] = curHeight
continue
case oracleexported.MsgAggregateExchangeRateVote:
err := spd.oracleKeeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, true)
if err != nil {
return err
}

valAddrStr := msg.Validator.String()
if lastSubmittedHeight, ok := spd.oracleVoteMap[valAddrStr]; ok && lastSubmittedHeight == curHeight {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "the validator has already been submitted vote at the current height")
}

spd.oracleVoteMap[valAddrStr] = curHeight
continue
default:
return nil
}
}

return nil
}
76 changes: 73 additions & 3 deletions x/auth/ante/spamming_prevention_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,81 @@ import (

"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/types"

"github.com/tendermint/tendermint/crypto"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/terra-project/core/x/auth/ante"
"github.com/terra-project/core/x/oracle"
)

func TestEnsureSoftforkGasCheck(t *testing.T) {
func TestOracleSpamming(t *testing.T) {
tempDir, err := ioutil.TempDir("", "wasmtest")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
viper.Set(flags.FlagHome, tempDir)

_, ctx := createTestApp()
_, _, addr1 := types.KeyTestPubAddr()
_, _, addr2 := types.KeyTestPubAddr()

spd := ante.NewSpammingPreventionDecorator(dummyOracleKeeper{
feeders: map[string]string{
sdk.ValAddress(addr1).String(): addr1.String(),
sdk.ValAddress(addr2).String(): addr2.String(),
},
})

// normal so ok
ctx = ctx.WithBlockHeight(100)
require.NoError(t, spd.CheckOracleSpamming(ctx, []sdk.Msg{
oracle.NewMsgAggregateExchangeRatePrevote(oracle.AggregateVoteHash{}, addr1, sdk.ValAddress(addr1)),
oracle.NewMsgAggregateExchangeRateVote("", "", addr1, sdk.ValAddress(addr1)),
}))

// do it again is blocked
require.Error(t, spd.CheckOracleSpamming(ctx, []sdk.Msg{
oracle.NewMsgAggregateExchangeRatePrevote(oracle.AggregateVoteHash{}, addr1, sdk.ValAddress(addr1)),
oracle.NewMsgAggregateExchangeRateVote("", "", addr1, sdk.ValAddress(addr1)),
}))

// next block; can put oracle again
ctx = ctx.WithBlockHeight(101)
require.NoError(t, spd.CheckOracleSpamming(ctx, []sdk.Msg{
oracle.NewMsgAggregateExchangeRatePrevote(oracle.AggregateVoteHash{}, addr1, sdk.ValAddress(addr1)),
oracle.NewMsgAggregateExchangeRateVote("", "", addr1, sdk.ValAddress(addr1)),
}))

// catch wrong feeder
ctx = ctx.WithBlockHeight(102)
require.Error(t, spd.CheckOracleSpamming(ctx, []sdk.Msg{
oracle.NewMsgAggregateExchangeRatePrevote(oracle.AggregateVoteHash{}, addr2, sdk.ValAddress(addr1)),
oracle.NewMsgAggregateExchangeRateVote("", "", addr1, sdk.ValAddress(addr1)),
}))

// catch wrong feeder
ctx = ctx.WithBlockHeight(103)
require.Error(t, spd.CheckOracleSpamming(ctx, []sdk.Msg{
oracle.NewMsgAggregateExchangeRatePrevote(oracle.AggregateVoteHash{}, addr1, sdk.ValAddress(addr1)),
oracle.NewMsgAggregateExchangeRateVote("", "", addr2, sdk.ValAddress(addr1)),
}))
}

func TestEnsureSoftforkGasCheck(t *testing.T) {
tempDir, err := ioutil.TempDir("", "wasmtest")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
viper.Set(flags.FlagHome, tempDir)

// setup
spd := ante.NewSpammingPreventionDecorator()
_, ctx := createTestApp()

spd := ante.NewSpammingPreventionDecorator(dummyOracleKeeper{
feeders: map[string]string{},
})
antehandler := sdk.ChainAnteDecorators(spd)

// keys and addresses
Expand Down Expand Up @@ -71,3 +129,15 @@ func TestEnsureSoftforkGasCheck(t *testing.T) {
_, err = antehandler(ctx, tx, false)
require.Error(t, err, "Decorator should have errored on high gas than hard cap")
}

type dummyOracleKeeper struct {
feeders map[string]string
}

func (ok dummyOracleKeeper) ValidateFeeder(ctx sdk.Context, feederAddr sdk.AccAddress, validatorAddr sdk.ValAddress, checkBonded bool) error {
if val, ok := ok.feeders[validatorAddr.String()]; ok && val == feederAddr.String() {
return nil
}

return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "cannot ensure feeder right")
}
18 changes: 17 additions & 1 deletion x/auth/ante/tax.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
core "github.com/terra-project/core/types"
marketexported "github.com/terra-project/core/x/market/exported"
msgauthexported "github.com/terra-project/core/x/msgauth/exported"
oracleexported "github.com/terra-project/core/x/oracle/exported"
wasmexported "github.com/terra-project/core/x/wasm/exported"
)

Expand Down Expand Up @@ -54,7 +55,7 @@ func (tfd TaxFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool,
taxes := FilterMsgAndComputeTax(ctx, tfd.treasuryKeeper, feeTx.GetMsgs())

// Mempool fee validation
if ctx.IsCheckTx() {
if ctx.IsCheckTx() && !isOracleTx(ctx, feeTx.GetMsgs()) {
if err := EnsureSufficientMempoolFees(ctx, gas, feeCoins, taxes); err != nil {
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, err.Error())
}
Expand Down Expand Up @@ -168,3 +169,18 @@ func computeTax(ctx sdk.Context, tk TreasuryKeeper, principal sdk.Coins) sdk.Coi

return taxes
}

func isOracleTx(ctx sdk.Context, msgs []sdk.Msg) bool {
for _, msg := range msgs {
switch msg.(type) {
case oracleexported.MsgAggregateExchangeRatePrevote:
continue
case oracleexported.MsgAggregateExchangeRateVote:
continue
default:
return false
}
}

return true
}
29 changes: 29 additions & 0 deletions x/auth/ante/tax_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/terra-project/core/x/auth/ante"
"github.com/terra-project/core/x/bank"
"github.com/terra-project/core/x/msgauth"
oracleexported "github.com/terra-project/core/x/oracle/exported"
"github.com/terra-project/core/x/treasury"
"github.com/terra-project/core/x/wasm"
wasmconfig "github.com/terra-project/core/x/wasm/config"
Expand Down Expand Up @@ -302,3 +303,31 @@ func TestEnsureMempoolFeesExecAuthorized(t *testing.T) {
_, err = antehandler(ctx, tx, false)
require.Nil(t, err, "Decorator should not have errored on fee higher than local gasPrice + tax")
}

func TestEnsureNoMempoolFeesForOracleMessages(t *testing.T) {
tempDir, err := ioutil.TempDir("", "wasmtest")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
viper.Set(flags.FlagHome, tempDir)

// setup
tapp, ctx := createTestApp()

lowGasPrice := []sdk.DecCoin{{Denom: "uusd", Amount: sdk.NewDec(100)}}
ctx = ctx.WithMinGasPrices(lowGasPrice)

tk := tapp.GetTreasuryKeeper()
mtd := ante.NewTaxFeeDecorator(tk)
antehandler := sdk.ChainAnteDecorators(mtd)

// keys and addresses
priv1, _, _ := types.KeyTestPubAddr()
privs, accNums, seqs := []crypto.PrivKey{priv1}, []uint64{0}, []uint64{0}

msgs := []sdk.Msg{oracleexported.MsgAggregateExchangeRatePrevote{}, oracleexported.MsgAggregateExchangeRateVote{}}

fee := auth.NewStdFee(100000, sdk.NewCoins())
tx := types.NewTestTx(ctx, msgs, privs, accNums, seqs, fee)
_, err = antehandler(ctx, tx, false)
require.NoError(t, err)
}
9 changes: 9 additions & 0 deletions x/oracle/exported/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// nolint:deadcode unused DONTCOVER
package exported

import "github.com/terra-project/core/x/oracle/internal/types"

type (
MsgAggregateExchangeRatePrevote = types.MsgAggregateExchangeRatePrevote
MsgAggregateExchangeRateVote = types.MsgAggregateExchangeRateVote
)
Loading

0 comments on commit 534de88

Please sign in to comment.