From b3f0d0adc7c501566938d06a71ea5a04c479e2b2 Mon Sep 17 00:00:00 2001 From: godismercilex <98415576+godismercilex@users.noreply.github.com> Date: Tue, 23 Aug 2022 14:44:50 +0200 Subject: [PATCH] feat(oracle): wire to the App (#832) * add: wire oracle * temp: attempt to fix localnet.sh * upgrade changelog * add: slash logging * add: miss counters logging * tmp * fix: almost there.... * add: finalize first oracle scenario integration test * add: function to find private key info for an address * fix: test * chore: move oracle to simapp * chore: lint * chore: docs and function simplification * chore: CHANGELOG.md * temp: move app_test.go to a separate pkg Co-authored-by: Agent Smith --- CHANGELOG.md | 1 + simapp/app.go | 25 ++++ x/oracle/abci.go | 2 + x/oracle/integration_test/app_test.go | 160 ++++++++++++++++++++++++++ x/oracle/keeper/msg_server.go | 5 +- x/oracle/keeper/slash.go | 1 + x/oracle/keeper/test_utils.go | 2 +- x/testutil/cli/network.go | 12 ++ x/testutil/cli/tx.go | 39 +++++++ 9 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 x/oracle/integration_test/app_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a0174615d..3c650e54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#813](https://github.com/NibiruChain/nibiru/pull/813) - (vpool): Expose mark price, mark TWAP, index price, and k (swap invariant) in the all-pools query * [#816](https://github.com/NibiruChain/nibiru/pull/816) - Remove tobin tax from x/oracle * [#810](https://github.com/NibiruChain/nibiru/pull/810) - feat(x/perp): expose 'marginRatioIndex' and block number on QueryTraderPosition +* [#832](https://github.com/NibiruChain/nibiru/pull/832) - x/oracle app wiring ### Documentation diff --git a/simapp/app.go b/simapp/app.go index dd8af2422..e8949d76e 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -8,6 +8,9 @@ import ( "os" "path/filepath" + "github.com/NibiruChain/nibiru/x/oracle" + oraclekeeper "github.com/NibiruChain/nibiru/x/oracle/keeper" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" _ "github.com/cosmos/cosmos-sdk/client/docs/statik" @@ -98,6 +101,8 @@ import ( tmos "github.com/tendermint/tendermint/libs/os" dbm "github.com/tendermint/tm-db" + oracletypes "github.com/NibiruChain/nibiru/x/oracle/types" + nibiapp "github.com/NibiruChain/nibiru/app" "github.com/NibiruChain/nibiru/x/common" "github.com/NibiruChain/nibiru/x/dex" @@ -174,6 +179,7 @@ var ( ibctransfer.AppModuleBasic{}, // native x/ + oracle.AppModuleBasic{}, dex.AppModuleBasic{}, pricefeed.AppModuleBasic{}, epochs.AppModuleBasic{}, @@ -201,6 +207,7 @@ var ( perptypes.FeePoolModuleAccount: {}, epochstypes.ModuleName: {}, lockuptypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + oracletypes.ModuleName: nil, stablecointypes.StableEFModuleAccount: {authtypes.Burner}, common.TreasuryPoolModuleAccount: {}, } @@ -269,6 +276,7 @@ type NibiruTestApp struct { // --------------- // Nibiru keepers // --------------- + OracleKeeper oraclekeeper.Keeper DexKeeper dexkeeper.Keeper StablecoinKeeper stablecoinkeeper.Keeper PerpKeeper perpkeeper.Keeper @@ -332,6 +340,7 @@ func NewNibiruTestApp( ibchost.StoreKey, ibctransfertypes.StoreKey, // nibiru x/ keys + oracletypes.StoreKey, dextypes.StoreKey, pricefeedtypes.StoreKey, stablecointypes.StoreKey, @@ -423,6 +432,14 @@ func NewNibiruTestApp( // ---------------------------------- Nibiru Chain x/ keepers + app.OracleKeeper = oraclekeeper.NewKeeper( + appCodec, + keys[oracletypes.StoreKey], + app.GetSubspace(oracletypes.ModuleName), + app.AccountKeeper, app.BankKeeper, app.DistrKeeper, &stakingKeeper, + distrtypes.ModuleName, + ) + app.DexKeeper = dexkeeper.NewKeeper( appCodec, keys[dextypes.StoreKey], app.GetSubspace(dextypes.ModuleName), app.AccountKeeper, app.BankKeeper, app.DistrKeeper) @@ -534,6 +551,9 @@ func NewNibiruTestApp( var skipGenesisInvariants = cast.ToBool( appOpts.Get(crisis.FlagSkipGenesisInvariants)) + oracleModule := oracle.NewAppModule( + appCodec, app.OracleKeeper, app.AccountKeeper, app.BankKeeper) + dexModule := dex.NewAppModule( appCodec, app.DexKeeper, app.AccountKeeper, app.BankKeeper) pricefeedModule := pricefeed.NewAppModule( @@ -575,6 +595,7 @@ func NewNibiruTestApp( params.NewAppModule(app.ParamsKeeper), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), // native x/ + oracleModule, dexModule, pricefeedModule, stablecoinModule, @@ -620,6 +641,7 @@ func NewNibiruTestApp( perptypes.ModuleName, lockuptypes.ModuleName, incentivizationtypes.ModuleName, + oracletypes.ModuleName, // ibc modules ibchost.ModuleName, ibctransfertypes.ModuleName, @@ -627,6 +649,7 @@ func NewNibiruTestApp( app.mm.SetOrderEndBlockers( crisistypes.ModuleName, govtypes.ModuleName, + oracletypes.ModuleName, stakingtypes.ModuleName, capabilitytypes.ModuleName, authtypes.ModuleName, @@ -678,6 +701,7 @@ func NewNibiruTestApp( upgradetypes.ModuleName, vestingtypes.ModuleName, // native x/ + oracletypes.ModuleName, dextypes.ModuleName, pricefeedtypes.ModuleName, epochstypes.ModuleName, @@ -992,6 +1016,7 @@ func initParamsKeeper( paramsKeeper.Subspace(pricefeedtypes.ModuleName) paramsKeeper.Subspace(epochstypes.ModuleName) paramsKeeper.Subspace(stablecointypes.ModuleName) + paramsKeeper.Subspace(oracletypes.ModuleName) // ibc params keepers paramsKeeper.Subspace(ibctransfertypes.ModuleName) paramsKeeper.Subspace(ibchost.ModuleName) diff --git a/x/oracle/abci.go b/x/oracle/abci.go index a6076f465..42ef65c36 100644 --- a/x/oracle/abci.go +++ b/x/oracle/abci.go @@ -16,6 +16,7 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { params := k.GetParams(ctx) if types.IsPeriodLastBlock(ctx, params.VotePeriod) { + k.Logger(ctx).Info("processing validator price votes") // Build claim map over all validators in active set validatorClaimMap := make(map[string]types.Claim) @@ -91,6 +92,7 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) { // Increase miss counter k.SetMissCounter(ctx, claim.Recipient, k.GetMissCounter(ctx, claim.Recipient)+1) + k.Logger(ctx).Info("vote miss", "validator", claim.Recipient.String()) } // Distribute rewards to ballot winners diff --git a/x/oracle/integration_test/app_test.go b/x/oracle/integration_test/app_test.go new file mode 100644 index 000000000..3e9d12e05 --- /dev/null +++ b/x/oracle/integration_test/app_test.go @@ -0,0 +1,160 @@ +package integration_test_test + +import ( + "context" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/simapp" + oracletypes "github.com/NibiruChain/nibiru/x/oracle/types" + testutilcli "github.com/NibiruChain/nibiru/x/testutil/cli" +) + +type IntegrationTestSuite struct { + suite.Suite + + cfg testutilcli.Config + network *testutilcli.Network +} + +func (s *IntegrationTestSuite) SetupTest() { + app.SetPrefixes(app.AccountAddressPrefix) + s.cfg = testutilcli.BuildNetworkConfig(simapp.NewTestGenesisStateFromDefault()) + s.cfg.NumValidators = 4 + s.cfg.GenesisState[oracletypes.ModuleName] = s.cfg.Codec.MustMarshalJSON(func() codec.ProtoMarshaler { + gs := oracletypes.DefaultGenesisState() + gs.Params.Whitelist = oracletypes.PairList{ + oracletypes.Pair{Name: "nibi:usdc"}, + oracletypes.Pair{Name: "btc:usdc"}, + } + + return gs + }()) + + s.network = testutilcli.NewNetwork(s.T(), s.cfg) + _, err := s.network.WaitForHeight(2) + require.NoError(s.T(), err) +} + +func (s *IntegrationTestSuite) TestSuccessfulVoting() { + // assuming validators have equal power + // we use the weighted median. + // what happens is that prices are ordered + // based on exchange rate, from lowest to highest. + // then the median is picked, based on consensus power + // so obviously, in this case, since validators have the same power + // once weight (based on power) >= total power (sum of weights) + // then the number picked is the one in the middle always. + prices := []map[string]sdk.Dec{ + { + "nibi:usdc": sdk.MustNewDecFromStr("1"), + "btc:usdc": sdk.MustNewDecFromStr("100203.0"), + }, + { + "nibi:usdc": sdk.MustNewDecFromStr("1"), + "btc:usdc": sdk.MustNewDecFromStr("100150.5"), + }, + { + "nibi:usdc": sdk.MustNewDecFromStr("1"), + "btc:usdc": sdk.MustNewDecFromStr("100200.9"), + }, + { + "nibi:usdc": sdk.MustNewDecFromStr("1"), + "btc:usdc": sdk.MustNewDecFromStr("100300.9"), + }, + } + votes := s.sendPrevotes(prices) + + s.waitVoteRevealBlock() + + s.sendVotes(votes) + + s.waitPriceUpdateBlock() + + gotPrices := s.currentPrices() + require.Equal(s.T(), + map[string]sdk.Dec{ + "nibi:usdc": sdk.MustNewDecFromStr("1"), + "btc:usdc": sdk.MustNewDecFromStr("100200.9"), + }, + gotPrices, + ) +} + +func (s *IntegrationTestSuite) sendPrevotes(prices []map[string]sdk.Dec) []string { + strVotes := make([]string, len(prices)) + for i, val := range s.network.Validators { + raw := prices[i] + votes := make(oracletypes.ExchangeRateTuples, 0, len(raw)) + for pair, price := range raw { + votes = append(votes, oracletypes.NewExchangeRateTuple(pair, price)) + } + + pricesStr, err := votes.ToString() + require.NoError(s.T(), err) + _, err = s.network.SendTx(val.Address, &oracletypes.MsgAggregateExchangeRatePrevote{ + Hash: oracletypes.GetAggregateVoteHash("1", pricesStr, val.ValAddress).String(), + Feeder: val.Address.String(), + Validator: val.ValAddress.String(), + }) + require.NoError(s.T(), err) + + strVotes[i] = pricesStr + } + + return strVotes +} + +func (s *IntegrationTestSuite) sendVotes(rates []string) { + for i, val := range s.network.Validators { + _, err := s.network.SendTx(val.Address, &oracletypes.MsgAggregateExchangeRateVote{ + Salt: "1", + ExchangeRates: rates[i], + Feeder: val.Address.String(), + Validator: val.ValAddress.String(), + }) + require.NoError(s.T(), err) + } +} + +func (s *IntegrationTestSuite) waitVoteRevealBlock() { + params, err := oracletypes.NewQueryClient(s.network.Validators[0].ClientCtx).Params(context.Background(), &oracletypes.QueryParamsRequest{}) + require.NoError(s.T(), err) + + votePeriod := params.Params.VotePeriod + + height, err := s.network.LatestHeight() + require.NoError(s.T(), err) + + waitBlock := (uint64(height)/votePeriod)*votePeriod + votePeriod + + _, err = s.network.WaitForHeight(int64(waitBlock + 1)) + require.NoError(s.T(), err) +} + +// it's an alias, but it exists to give better understanding of what we're doing in test cases scenarios +func (s *IntegrationTestSuite) waitPriceUpdateBlock() { + s.waitVoteRevealBlock() +} + +func (s *IntegrationTestSuite) currentPrices() map[string]sdk.Dec { + rawRates, err := oracletypes.NewQueryClient(s.network.Validators[0].ClientCtx).ExchangeRates(context.Background(), &oracletypes.QueryExchangeRatesRequest{}) + require.NoError(s.T(), err) + + prices := make(map[string]sdk.Dec) + + for _, p := range rawRates.ExchangeRates { + prices[p.Pair] = p.ExchangeRate + } + + return prices +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/x/oracle/keeper/msg_server.go b/x/oracle/keeper/msg_server.go index 95a118bc1..3d8673669 100644 --- a/x/oracle/keeper/msg_server.go +++ b/x/oracle/keeper/msg_server.go @@ -90,7 +90,10 @@ func (ms msgServer) AggregateExchangeRateVote(goCtx context.Context, msg *types. // Check a msg is submitted proper period if (uint64(ctx.BlockHeight())/params.VotePeriod)-(aggregatePrevote.SubmitBlock/params.VotePeriod) != 1 { - return nil, types.ErrRevealPeriodMissMatch + return nil, types.ErrRevealPeriodMissMatch.Wrapf( + "aggregate prevote block: %d, current block: %d, vote period: %d", + aggregatePrevote.SubmitBlock, ctx.BlockHeight(), params.VotePeriod, + ) } exchangeRateTuples, err := types.ParseExchangeRateTuples(msg.ExchangeRates) diff --git a/x/oracle/keeper/slash.go b/x/oracle/keeper/slash.go index 55ea85796..af673a4bd 100644 --- a/x/oracle/keeper/slash.go +++ b/x/oracle/keeper/slash.go @@ -38,6 +38,7 @@ func (k Keeper) SlashAndResetMissCounters(ctx sdk.Context) { ctx, consAddr, distributionHeight, validator.GetConsensusPower(powerReduction), slashFraction, ) + k.Logger(ctx).Info("slash", "validator", consAddr.String(), "fraction", slashFraction.String()) k.StakingKeeper.Jail(ctx, consAddr) } } diff --git a/x/oracle/keeper/test_utils.go b/x/oracle/keeper/test_utils.go index 904dc4c71..4d48451a2 100644 --- a/x/oracle/keeper/test_utils.go +++ b/x/oracle/keeper/test_utils.go @@ -3,6 +3,7 @@ package keeper import ( "github.com/NibiruChain/nibiru/x/common" + "github.com/cosmos/cosmos-sdk/std" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" distr "github.com/cosmos/cosmos-sdk/x/distribution" @@ -26,7 +27,6 @@ import ( cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/simapp" simparams "github.com/cosmos/cosmos-sdk/simapp/params" - "github.com/cosmos/cosmos-sdk/std" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" diff --git a/x/testutil/cli/network.go b/x/testutil/cli/network.go index b3363dc6a..00f6ed6eb 100644 --- a/x/testutil/cli/network.go +++ b/x/testutil/cli/network.go @@ -517,3 +517,15 @@ func (n *Network) Cleanup() { n.T.Log("finished cleaning up test network") } + +func (n *Network) keyBaseAndInfoForAddr(addr sdk.AccAddress) (keyring.Keyring, keyring.Info) { + for _, v := range n.Validators { + info, err := v.ClientCtx.Keyring.KeyByAddress(addr) + if err == nil { + return v.ClientCtx.Keyring, info + } + } + + n.T.Fatalf("address not found in any of the known validators keyrings: %s", addr.String()) + return nil, nil +} diff --git a/x/testutil/cli/tx.go b/x/testutil/cli/tx.go index e82aae86b..8292d58c3 100644 --- a/x/testutil/cli/tx.go +++ b/x/testutil/cli/tx.go @@ -1,12 +1,15 @@ package cli import ( + "context" "fmt" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/testutil/cli" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" + "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/abci/types" "github.com/NibiruChain/nibiru/x/common" @@ -96,3 +99,39 @@ func ExecTx(network *Network, cmd *cobra.Command, txSender sdk.AccAddress, args return resp, nil } + +func (n *Network) SendTx(addr sdk.AccAddress, msgs ...sdk.Msg) (*sdk.TxResponse, error) { + cfg := n.Config + kb, info := n.keyBaseAndInfoForAddr(addr) + rpc := n.Validators[0].RPCClient + txBuilder := cfg.TxConfig.NewTxBuilder() + require.NoError(n.T, txBuilder.SetMsgs(msgs...)) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(cfg.BondDenom, sdk.NewInt(1)))) + txBuilder.SetGasLimit(1000000) + + acc, err := cfg.AccountRetriever.GetAccount(n.Validators[0].ClientCtx, addr) + require.NoError(n.T, err) + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(cfg.ChainID). + WithKeybase(kb). + WithTxConfig(cfg.TxConfig). + WithAccountRetriever(cfg.AccountRetriever). + WithAccountNumber(acc.GetAccountNumber()). + WithSequence(acc.GetSequence()) + + err = tx.Sign(txFactory, info.GetName(), txBuilder, true) + require.NoError(n.T, err) + + txBytes, err := cfg.TxConfig.TxEncoder()(txBuilder.GetTx()) + require.NoError(n.T, err) + + respRaw, err := rpc.BroadcastTxCommit(context.Background(), txBytes) + require.NoError(n.T, err) + + require.Truef(n.T, respRaw.CheckTx.IsOK(), "tx failed: %s", respRaw.CheckTx.Log) + require.Truef(n.T, respRaw.DeliverTx.IsOK(), "tx failed: %s", respRaw.DeliverTx.Log) + + return sdk.NewResponseFormatBroadcastTxCommit(respRaw), nil +}