From 534de88dbac0519f6f852ed411905b71a6c42e4e Mon Sep 17 00:00:00 2001 From: yys Date: Thu, 18 Mar 2021 19:49:34 +0900 Subject: [PATCH] [Feature] lower oracle feeder cost (#463) * 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 --- app/app.go | 7 +- x/auth/ante/ante.go | 13 ++-- x/auth/ante/expected_keeper.go | 7 +- x/auth/ante/spamming_prevention.go | 73 ++++++++++++++++++++- x/auth/ante/spamming_prevention_test.go | 76 +++++++++++++++++++++- x/auth/ante/tax.go | 18 ++++- x/auth/ante/tax_test.go | 29 +++++++++ x/oracle/exported/alias.go | 9 +++ x/oracle/handler.go | 58 ++++------------- x/oracle/internal/keeper/keeper.go | 31 +++++++++ x/oracle/internal/keeper/keeper_test.go | 42 ++++++++++++ x/oracle/internal/types/expected_keeper.go | 1 + x/oracle/internal/types/test_utils.go | 6 ++ x/oracle/simulation/operations.go | 9 ++- 14 files changed, 318 insertions(+), 61 deletions(-) create mode 100644 x/oracle/exported/alias.go diff --git a/app/app.go b/app/app.go index 6c41d7ec3..9a37358d6 100644 --- a/app/app.go +++ b/app/app.go @@ -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 { diff --git a/x/auth/ante/ante.go b/x/auth/ante/ante.go index ecb9aaa12..deabe58e1 100644 --- a/x/auth/ante/ante.go +++ b/x/auth/ante/ante.go @@ -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), diff --git a/x/auth/ante/expected_keeper.go b/x/auth/ante/expected_keeper.go index d6be6d45d..85bd1e38b 100644 --- a/x/auth/ante/expected_keeper.go +++ b/x/auth/ante/expected_keeper.go @@ -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 +} diff --git a/x/auth/ante/spamming_prevention.go b/x/auth/ante/spamming_prevention.go index dba51c39d..6567c771b 100644 --- a/x/auth/ante/spamming_prevention.go +++ b/x/auth/ante/spamming_prevention.go @@ -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 @@ -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 @@ -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") } } @@ -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 +} diff --git a/x/auth/ante/spamming_prevention_test.go b/x/auth/ante/spamming_prevention_test.go index 92ab238a8..f6de2d03c 100644 --- a/x/auth/ante/spamming_prevention_test.go +++ b/x/auth/ante/spamming_prevention_test.go @@ -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 @@ -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") +} diff --git a/x/auth/ante/tax.go b/x/auth/ante/tax.go index 018c3123d..f9c596a4c 100644 --- a/x/auth/ante/tax.go +++ b/x/auth/ante/tax.go @@ -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" ) @@ -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()) } @@ -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 +} diff --git a/x/auth/ante/tax_test.go b/x/auth/ante/tax_test.go index 127aae2e7..bc2217521 100644 --- a/x/auth/ante/tax_test.go +++ b/x/auth/ante/tax_test.go @@ -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" @@ -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) +} diff --git a/x/oracle/exported/alias.go b/x/oracle/exported/alias.go new file mode 100644 index 000000000..f9cdcb4c1 --- /dev/null +++ b/x/oracle/exported/alias.go @@ -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 +) diff --git a/x/oracle/handler.go b/x/oracle/handler.go index 5ca69b506..9c171f2ad 100644 --- a/x/oracle/handler.go +++ b/x/oracle/handler.go @@ -46,17 +46,9 @@ func handleMsgExchangeRatePrevote(ctx sdk.Context, keeper Keeper, msg MsgExchang return nil, sdkerrors.Wrap(ErrUnknownDenom, msg.Denom) } - if !msg.Feeder.Equals(msg.Validator) { - delegate := keeper.GetOracleDelegate(ctx, msg.Validator) - if !delegate.Equals(msg.Feeder) { - return nil, sdkerrors.Wrap(ErrNoVotingPermission, msg.Feeder.String()) - } - } - - // Check that the given validator exists - val := keeper.StakingKeeper.Validator(ctx, msg.Validator) - if val == nil { - return nil, sdkerrors.Wrap(staking.ErrNoValidatorFound, msg.Validator.String()) + err := keeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, !core.IsWaitingForSoftfork(ctx, 3)) + if err != nil { + return nil, err } prevote := NewExchangeRatePrevote(msg.Hash, msg.Denom, msg.Validator, ctx.BlockHeight()) @@ -80,17 +72,9 @@ func handleMsgExchangeRatePrevote(ctx sdk.Context, keeper Keeper, msg MsgExchang // handleMsgExchangeRateVote handles a MsgExchangeRateVote func handleMsgExchangeRateVote(ctx sdk.Context, keeper Keeper, msg MsgExchangeRateVote) (*sdk.Result, error) { - if !msg.Feeder.Equals(msg.Validator) { - delegate := keeper.GetOracleDelegate(ctx, msg.Validator) - if !delegate.Equals(msg.Feeder) { - return nil, sdkerrors.Wrap(ErrNoVotingPermission, msg.Feeder.String()) - } - } - - // Check that the given validator exists - val := keeper.StakingKeeper.Validator(ctx, msg.Validator) - if val == nil { - return nil, sdkerrors.Wrap(staking.ErrNoValidatorFound, msg.Validator.String()) + err := keeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, !core.IsWaitingForSoftfork(ctx, 3)) + if err != nil { + return nil, err } params := keeper.GetParams(ctx) @@ -164,17 +148,9 @@ func handleMsgDelegateFeedConsent(ctx sdk.Context, keeper Keeper, msg MsgDelegat // handleMsgAggregateExchangeRatePrevote handles a MsgAggregateExchangeRatePrevote func handleMsgAggregateExchangeRatePrevote(ctx sdk.Context, keeper Keeper, msg MsgAggregateExchangeRatePrevote) (*sdk.Result, error) { - if !msg.Feeder.Equals(msg.Validator) { - delegate := keeper.GetOracleDelegate(ctx, msg.Validator) - if !delegate.Equals(msg.Feeder) { - return nil, sdkerrors.Wrap(ErrNoVotingPermission, msg.Feeder.String()) - } - } - - // Check that the given validator exists - val := keeper.StakingKeeper.Validator(ctx, msg.Validator) - if val == nil { - return nil, sdkerrors.Wrap(staking.ErrNoValidatorFound, msg.Validator.String()) + err := keeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, !core.IsWaitingForSoftfork(ctx, 3)) + if err != nil { + return nil, err } aggregatePrevote := NewAggregateExchangeRatePrevote(msg.Hash, msg.Validator, ctx.BlockHeight()) @@ -197,17 +173,9 @@ func handleMsgAggregateExchangeRatePrevote(ctx sdk.Context, keeper Keeper, msg M // handleMsgAggregateExchangeRateVote handles a MsgAggregateExchangeRateVote func handleMsgAggregateExchangeRateVote(ctx sdk.Context, keeper Keeper, msg MsgAggregateExchangeRateVote) (*sdk.Result, error) { - if !msg.Feeder.Equals(msg.Validator) { - delegate := keeper.GetOracleDelegate(ctx, msg.Validator) - if !delegate.Equals(msg.Feeder) { - return nil, sdkerrors.Wrap(ErrNoVotingPermission, msg.Feeder.String()) - } - } - - // Check that the given validator exists - val := keeper.StakingKeeper.Validator(ctx, msg.Validator) - if val == nil { - return nil, sdkerrors.Wrap(staking.ErrNoValidatorFound, msg.Validator.String()) + err := keeper.ValidateFeeder(ctx, msg.Feeder, msg.Validator, !core.IsWaitingForSoftfork(ctx, 3)) + if err != nil { + return nil, err } params := keeper.GetParams(ctx) @@ -217,7 +185,7 @@ func handleMsgAggregateExchangeRateVote(ctx sdk.Context, keeper Keeper, msg MsgA return nil, sdkerrors.Wrap(ErrNoAggregatePrevote, msg.Validator.String()) } - // Check a msg is submitted porper period + // Check a msg is submitted proper period if (ctx.BlockHeight()/params.VotePeriod)-(aggregatePrevote.SubmitBlock/params.VotePeriod) != 1 { return nil, ErrRevealPeriodMissMatch } diff --git a/x/oracle/internal/keeper/keeper.go b/x/oracle/internal/keeper/keeper.go index 638069f24..739b0ceb5 100644 --- a/x/oracle/internal/keeper/keeper.go +++ b/x/oracle/internal/keeper/keeper.go @@ -9,6 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/params" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" core "github.com/terra-project/core/types" "github.com/terra-project/core/x/oracle/internal/types" @@ -450,3 +451,33 @@ func (k Keeper) ClearTobinTaxes(ctx sdk.Context) { store.Delete(iter.Key()) } } + +// ValidateFeeder return the given feeder is allowed to feed the message or not +func (k Keeper) ValidateFeeder(ctx sdk.Context, feederAddr sdk.AccAddress, validatorAddr sdk.ValAddress, checkBonded bool) error { + if !feederAddr.Equals(validatorAddr) { + delegate := k.GetOracleDelegate(ctx, validatorAddr) + if !delegate.Equals(feederAddr) { + return sdkerrors.Wrap(types.ErrNoVotingPermission, feederAddr.String()) + } + } + + // Check that the given validator exists + val := k.StakingKeeper.Validator(ctx, validatorAddr) + if val == nil { + return sdkerrors.Wrap(stakingtypes.ErrNoValidatorFound, validatorAddr.String()) + } + + // only used in mempool check + // TODO - remove checkBonded flag at columbus-5 + if checkBonded { + if !val.IsBonded() { + return sdkerrors.Wrapf(stakingtypes.ErrNoValidatorFound, "validator %s is not bonded state", validatorAddr.String()) + } + + if k.StakingKeeper.GetLastValidatorPower(ctx, validatorAddr) == 0 { + return sdkerrors.Wrapf(stakingtypes.ErrNoValidatorFound, "validator %s is not active set", validatorAddr.String()) + } + } + + return nil +} diff --git a/x/oracle/internal/keeper/keeper_test.go b/x/oracle/internal/keeper/keeper_test.go index 04fc0acb1..5839dce4e 100644 --- a/x/oracle/internal/keeper/keeper_test.go +++ b/x/oracle/internal/keeper/keeper_test.go @@ -10,6 +10,7 @@ import ( "github.com/terra-project/core/x/oracle/internal/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" ) func TestPrevoteAddDelete(t *testing.T) { @@ -471,3 +472,44 @@ func TestTobinTaxGetSet(t *testing.T) { require.Error(t, err) } } + +func TestValidateFeeder(t *testing.T) { + // initial setup + input := CreateTestInput(t) + addr, val := ValAddrs[0], PubKeys[0] + addr1, val1 := ValAddrs[1], PubKeys[1] + amt := sdk.TokensFromConsensusPower(100) + sh := staking.NewHandler(input.StakingKeeper) + ctx := input.Ctx + + // Validator created + _, err := sh(ctx, NewTestMsgCreateValidator(addr, val, amt)) + require.NoError(t, err) + _, err = sh(ctx, NewTestMsgCreateValidator(addr1, val1, amt)) + require.NoError(t, err) + staking.EndBlocker(ctx, input.StakingKeeper) + + require.Equal( + t, input.BankKeeper.GetCoins(ctx, sdk.AccAddress(addr)), + sdk.NewCoins(sdk.NewCoin(input.StakingKeeper.GetParams(ctx).BondDenom, InitTokens.Sub(amt))), + ) + require.Equal(t, amt, input.StakingKeeper.Validator(ctx, addr).GetBondedTokens()) + require.Equal( + t, input.BankKeeper.GetCoins(ctx, sdk.AccAddress(addr1)), + sdk.NewCoins(sdk.NewCoin(input.StakingKeeper.GetParams(ctx).BondDenom, InitTokens.Sub(amt))), + ) + require.Equal(t, amt, input.StakingKeeper.Validator(ctx, addr1).GetBondedTokens()) + + require.NoError(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(addr), sdk.ValAddress(addr), true)) + require.NoError(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(addr1), sdk.ValAddress(addr1), true)) + + // delegate works + input.OracleKeeper.SetOracleDelegate(input.Ctx, sdk.ValAddress(addr), sdk.AccAddress(addr1)) + require.NoError(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(addr1), sdk.ValAddress(addr), true)) + require.Error(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(Addrs[2]), sdk.ValAddress(addr), true)) + + // only active validators can do oracle votes + input.StakingKeeper.SetLastValidatorPower(input.Ctx, sdk.ValAddress(addr), 0) + require.NoError(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(addr1), sdk.ValAddress(addr), false)) + require.Error(t, input.OracleKeeper.ValidateFeeder(input.Ctx, sdk.AccAddress(addr1), sdk.ValAddress(addr), true)) +} diff --git a/x/oracle/internal/types/expected_keeper.go b/x/oracle/internal/types/expected_keeper.go index 9c190f10e..7ce4b3595 100644 --- a/x/oracle/internal/types/expected_keeper.go +++ b/x/oracle/internal/types/expected_keeper.go @@ -13,6 +13,7 @@ type StakingKeeper interface { Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec) // slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction Jail(sdk.Context, sdk.ConsAddress) // jail a validator IterateValidators(sdk.Context, func(index int64, validator stakingexported.ValidatorI) (stop bool)) + GetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) (power int64) } // DistributionKeeper is expected keeper for distribution module diff --git a/x/oracle/internal/types/test_utils.go b/x/oracle/internal/types/test_utils.go index 7bf72ed94..b2a379d14 100644 --- a/x/oracle/internal/types/test_utils.go +++ b/x/oracle/internal/types/test_utils.go @@ -87,6 +87,12 @@ func (DummyStakingKeeper) IterateValidators(sdk.Context, func(index int64, valid func (DummyStakingKeeper) Jail(sdk.Context, sdk.ConsAddress) { } +// GetLastValidatorPower nolint +func (sk DummyStakingKeeper) GetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) (power int64) { + return sk.Validator(ctx, operator).GetConsensusPower() +} + +// MockValidator nolint type MockValidator struct { power int64 operator sdk.ValAddress diff --git a/x/oracle/simulation/operations.go b/x/oracle/simulation/operations.go index 95f665690..c5120f8a5 100644 --- a/x/oracle/simulation/operations.go +++ b/x/oracle/simulation/operations.go @@ -89,7 +89,8 @@ func SimulateMsgExchangeRatePrevote(ak authkeeper.AccountKeeper, k keeper.Keeper // ensure the validator exists val := k.StakingKeeper.Validator(ctx, address) - if val == nil { + power := k.StakingKeeper.GetLastValidatorPower(ctx, address) + if val == nil || !val.IsBonded() || power == 0 { return simulation.NoOpMsg(types.ModuleName), nil, nil } @@ -141,7 +142,8 @@ func SimulateMsgExchangeRateVote(ak authkeeper.AccountKeeper, k keeper.Keeper) s // ensure the validator exists val := k.StakingKeeper.Validator(ctx, address) - if val == nil { + power := k.StakingKeeper.GetLastValidatorPower(ctx, address) + if val == nil || !val.IsBonded() || power == 0 { return simulation.NoOpMsg(types.ModuleName), nil, nil } @@ -208,7 +210,8 @@ func SimulateMsgDelegateFeedConsent(ak authkeeper.AccountKeeper, k keeper.Keeper // ensure the validator exists val := k.StakingKeeper.Validator(ctx, valAddress) - if val == nil { + power := k.StakingKeeper.GetLastValidatorPower(ctx, valAddress) + if val == nil || !val.IsBonded() || power == 0 { return simulation.NoOpMsg(types.ModuleName), nil, nil }