diff --git a/.pending/features/gaia/2935-optionally-asse b/.pending/features/gaia/2935-optionally-asse new file mode 100644 index 000000000000..2832fe8e6fa3 --- /dev/null +++ b/.pending/features/gaia/2935-optionally-asse @@ -0,0 +1 @@ +#2935 Optionally assert invariants on a blockly basis using `gaiad --assert-invariants-blockly` diff --git a/.pending/features/sdk/2935-New-module-Cris b/.pending/features/sdk/2935-New-module-Cris new file mode 100644 index 000000000000..f036ad14d656 --- /dev/null +++ b/.pending/features/sdk/2935-New-module-Cris @@ -0,0 +1 @@ +#2935 New module Crisis which can test broken invariant with messages diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index d9a4a0e7e42e..7d47b121be53 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -220,7 +220,7 @@ func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress privVal.Reset() db := dbm.NewMemDB() - app := gapp.NewGaiaApp(logger, db, nil, true) + app := gapp.NewGaiaApp(logger, db, nil, true, false) cdc = gapp.MakeCodec() genesisFile := config.GenesisFile() @@ -299,6 +299,9 @@ func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress genesisState.MintData.Minter.Inflation = inflationMin genesisState.MintData.Params.InflationMin = inflationMin + // initialize crisis data + genesisState.CrisisData.ConstantFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000) + // double check inflation is set according to the minting boolean flag if minting { require.Equal(t, sdk.MustNewDecFromStr("15000.0"), diff --git a/client/utils/utils.go b/client/utils/utils.go index 6e8aec8b466d..25d81c4518e4 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -123,7 +123,9 @@ func EnrichWithGas(txBldr authtxb.TxBuilder, cliCtx context.CLIContext, msgs []s // CalculateGas simulates the execution of a transaction and returns // both the estimate obtained by the query and the adjusted amount. -func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc *amino.Codec, txBytes []byte, adjustment float64) (estimate, adjusted uint64, err error) { +func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), + cdc *amino.Codec, txBytes []byte, adjustment float64) (estimate, adjusted uint64, err error) { + // run a simulation (via /app/simulate query) to // estimate gas and update TxBuilder accordingly rawRes, err := queryFunc("/app/simulate", txBytes) diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 36044513f3d3..bd2c3d92b811 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -19,6 +19,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" @@ -44,6 +45,8 @@ type GaiaApp struct { *bam.BaseApp cdc *codec.Codec + assertInvariantsBlockly bool + // keys to access the substores keyMain *sdk.KVStoreKey keyAccount *sdk.KVStoreKey @@ -67,11 +70,14 @@ type GaiaApp struct { mintKeeper mint.Keeper distrKeeper distr.Keeper govKeeper gov.Keeper + crisisKeeper crisis.Keeper paramsKeeper params.Keeper } // NewGaiaApp returns a reference to an initialized GaiaApp. -func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, baseAppOptions ...func(*bam.BaseApp)) *GaiaApp { +func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest, assertInvariantsBlockly bool, + baseAppOptions ...func(*bam.BaseApp)) *GaiaApp { + cdc := MakeCodec() bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOptions...) @@ -143,6 +149,12 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b app.paramsKeeper, app.paramsKeeper.Subspace(gov.DefaultParamspace), app.bankKeeper, &stakingKeeper, gov.DefaultCodespace, ) + app.crisisKeeper = crisis.NewKeeper( + app.paramsKeeper.Subspace(crisis.DefaultParamspace), + app.distrKeeper, + app.bankKeeper, + app.feeCollectionKeeper, + ) // register the staking hooks // NOTE: The stakingKeeper above is passed by reference, so that it can be @@ -151,6 +163,11 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b NewStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks()), ) + // register the crisis routes + bank.RegisterInvariants(&app.crisisKeeper, app.accountKeeper) + distr.RegisterInvariants(&app.crisisKeeper, app.distrKeeper, app.stakingKeeper) + staking.RegisterInvariants(&app.crisisKeeper, app.stakingKeeper, app.feeCollectionKeeper, app.distrKeeper, app.accountKeeper) + // register message routes // // TODO: Use standard bank router once transfers are enabled. @@ -159,7 +176,8 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b AddRoute(staking.RouterKey, staking.NewHandler(app.stakingKeeper)). AddRoute(distr.RouterKey, distr.NewHandler(app.distrKeeper)). AddRoute(slashing.RouterKey, slashing.NewHandler(app.slashingKeeper)). - AddRoute(gov.RouterKey, gov.NewHandler(app.govKeeper)) + AddRoute(gov.RouterKey, gov.NewHandler(app.govKeeper)). + AddRoute(crisis.RouterKey, crisis.NewHandler(app.crisisKeeper)) app.QueryRouter(). AddRoute(auth.QuerierRoute, auth.NewQuerier(app.accountKeeper)). @@ -197,6 +215,7 @@ func MakeCodec() *codec.Codec { slashing.RegisterCodec(cdc) gov.RegisterCodec(cdc) auth.RegisterCodec(cdc) + crisis.RegisterCodec(cdc) sdk.RegisterCodec(cdc) codec.RegisterCrypto(cdc) return cdc @@ -229,7 +248,9 @@ func (app *GaiaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.R validatorUpdates, endBlockerTags := staking.EndBlocker(ctx, app.stakingKeeper) tags = append(tags, endBlockerTags...) - app.assertRuntimeInvariants() + if app.assertInvariantsBlockly { + app.assertRuntimeInvariants() + } return abci.ResponseEndBlock{ ValidatorUpdates: validatorUpdates, @@ -262,6 +283,7 @@ func (app *GaiaApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisSt bank.InitGenesis(ctx, app.bankKeeper, genesisState.BankData) slashing.InitGenesis(ctx, app.slashingKeeper, genesisState.SlashingData, genesisState.StakingData.Validators.ToSDKValidators()) gov.InitGenesis(ctx, app.govKeeper, genesisState.GovData) + crisis.InitGenesis(ctx, app.crisisKeeper, genesisState.CrisisData) mint.InitGenesis(ctx, app.mintKeeper, genesisState.MintData) // validate genesis state diff --git a/cmd/gaia/app/app_test.go b/cmd/gaia/app/app_test.go index 0ee1596ea878..ec38f06a671b 100644 --- a/cmd/gaia/app/app_test.go +++ b/cmd/gaia/app/app_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/db" @@ -35,6 +36,7 @@ func setGenesis(gapp *GaiaApp, accs ...*auth.BaseAccount) error { mint.DefaultGenesisState(), distr.DefaultGenesisState(), gov.DefaultGenesisState(), + crisis.DefaultGenesisState(), slashing.DefaultGenesisState(), ) @@ -53,11 +55,11 @@ func setGenesis(gapp *GaiaApp, accs ...*auth.BaseAccount) error { func TestGaiadExport(t *testing.T) { db := db.NewMemDB() - gapp := NewGaiaApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true) + gapp := NewGaiaApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, false) setGenesis(gapp) // Making a new app object with the db, so that initchain hasn't been called - newGapp := NewGaiaApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true) + newGapp := NewGaiaApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, false) _, _, err := newGapp.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") } diff --git a/cmd/gaia/app/export.go b/cmd/gaia/app/export.go index 3bd439396262..5174c1498a92 100644 --- a/cmd/gaia/app/export.go +++ b/cmd/gaia/app/export.go @@ -11,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" @@ -46,6 +47,7 @@ func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteLis mint.ExportGenesis(ctx, app.mintKeeper), distr.ExportGenesis(ctx, app.distrKeeper), gov.ExportGenesis(ctx, app.govKeeper), + crisis.ExportGenesis(ctx, app.crisisKeeper), slashing.ExportGenesis(ctx, app.slashingKeeper), ) appState, err = codec.MarshalJSONIndent(app.cdc, genState) @@ -118,6 +120,7 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []st // reinitialize all delegations for _, del := range dels { app.distrKeeper.Hooks().BeforeDelegationCreated(ctx, del.DelegatorAddress, del.ValidatorAddress) + app.distrKeeper.Hooks().AfterDelegationModified(ctx, del.DelegatorAddress, del.ValidatorAddress) } // reset context height diff --git a/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index 65a64033ba15..997cd131c751 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -17,6 +17,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" @@ -39,6 +40,7 @@ type GenesisState struct { MintData mint.GenesisState `json:"mint"` DistrData distr.GenesisState `json:"distr"` GovData gov.GenesisState `json:"gov"` + CrisisData crisis.GenesisState `json:"crisis"` SlashingData slashing.GenesisState `json:"slashing"` GenTxs []json.RawMessage `json:"gentxs"` } @@ -46,7 +48,7 @@ type GenesisState struct { func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, bankData bank.GenesisState, stakingData staking.GenesisState, mintData mint.GenesisState, - distrData distr.GenesisState, govData gov.GenesisState, + distrData distr.GenesisState, govData gov.GenesisState, crisisData crisis.GenesisState, slashingData slashing.GenesisState) GenesisState { return GenesisState{ @@ -57,6 +59,7 @@ func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, MintData: mintData, DistrData: distrData, GovData: govData, + CrisisData: crisisData, SlashingData: slashingData, } } @@ -209,6 +212,7 @@ func NewDefaultGenesisState() GenesisState { MintData: mint.DefaultGenesisState(), DistrData: distr.DefaultGenesisState(), GovData: gov.DefaultGenesisState(), + CrisisData: crisis.DefaultGenesisState(), SlashingData: slashing.DefaultGenesisState(), GenTxs: nil, } @@ -246,6 +250,9 @@ func GaiaValidateGenesisState(genesisState GenesisState) error { if err := gov.ValidateGenesis(genesisState.GovData); err != nil { return err } + if err := crisis.ValidateGenesis(genesisState.CrisisData); err != nil { + return err + } return slashing.ValidateGenesis(genesisState.SlashingData) } diff --git a/cmd/gaia/app/invariants.go b/cmd/gaia/app/invariants.go index 535cbff3711c..841732ca11e1 100644 --- a/cmd/gaia/app/invariants.go +++ b/cmd/gaia/app/invariants.go @@ -7,20 +7,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" - banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" - distrsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation" - stakingsim "github.com/cosmos/cosmos-sdk/x/staking/simulation" ) -func (app *GaiaApp) runtimeInvariants() []sdk.Invariant { - return []sdk.Invariant{ - banksim.NonnegativeBalanceInvariant(app.accountKeeper), - distrsim.NonNegativeOutstandingInvariant(app.distrKeeper), - stakingsim.SupplyInvariants(app.stakingKeeper, app.feeCollectionKeeper, app.distrKeeper, app.accountKeeper), - stakingsim.NonNegativePowerInvariant(app.stakingKeeper), - } -} - func (app *GaiaApp) assertRuntimeInvariants() { ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1}) app.assertRuntimeInvariantsOnContext(ctx) @@ -28,10 +16,12 @@ func (app *GaiaApp) assertRuntimeInvariants() { func (app *GaiaApp) assertRuntimeInvariantsOnContext(ctx sdk.Context) { start := time.Now() - invariants := app.runtimeInvariants() - for _, inv := range invariants { - if err := inv(ctx); err != nil { - panic(fmt.Errorf("invariant broken: %s", err)) + invarRoutes := app.crisisKeeper.Routes() + for _, ir := range invarRoutes { + if err := ir.Invar(ctx); err != nil { + panic(fmt.Errorf("invariant broken: %s\n"+ + "\tCRITICAL please submit the following transaction:\n"+ + "\t\t gaiacli tx crisis invariant-broken %v %v", err, ir.ModuleName, ir.Route)) } } end := time.Now() diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index d1211a460334..247cc262a308 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -294,12 +294,10 @@ func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { func invariants(app *GaiaApp) []sdk.Invariant { return []sdk.Invariant{ - simulation.PeriodicInvariant(banksim.NonnegativeBalanceInvariant(app.accountKeeper), period, 0), - simulation.PeriodicInvariant(govsim.AllInvariants(), period, 0), - simulation.PeriodicInvariant(distrsim.AllInvariants(app.distrKeeper, app.stakingKeeper), period, 0), - simulation.PeriodicInvariant(stakingsim.AllInvariants(app.stakingKeeper, app.feeCollectionKeeper, + simulation.PeriodicInvariant(bank.NonnegativeBalanceInvariant(app.accountKeeper), period, 0), + simulation.PeriodicInvariant(distr.AllInvariants(app.distrKeeper, app.stakingKeeper), period, 0), + simulation.PeriodicInvariant(staking.AllInvariants(app.stakingKeeper, app.feeCollectionKeeper, app.distrKeeper, app.accountKeeper), period, 0), - simulation.PeriodicInvariant(slashingsim.AllInvariants(), period, 0), } } @@ -321,7 +319,7 @@ func BenchmarkFullGaiaSimulation(b *testing.B) { db.Close() os.RemoveAll(dir) }() - app := NewGaiaApp(logger, db, nil, true) + app := NewGaiaApp(logger, db, nil, true, false) // Run randomized simulation // TODO parameterize numbers, save for a later PR @@ -356,7 +354,7 @@ func TestFullGaiaSimulation(t *testing.T) { db.Close() os.RemoveAll(dir) }() - app := NewGaiaApp(logger, db, nil, true, fauxMerkleModeOpt) + app := NewGaiaApp(logger, db, nil, true, false, fauxMerkleModeOpt) require.Equal(t, "GaiaApp", app.Name()) // Run randomized simulation @@ -390,7 +388,7 @@ func TestGaiaImportExport(t *testing.T) { db.Close() os.RemoveAll(dir) }() - app := NewGaiaApp(logger, db, nil, true, fauxMerkleModeOpt) + app := NewGaiaApp(logger, db, nil, true, false, fauxMerkleModeOpt) require.Equal(t, "GaiaApp", app.Name()) // Run randomized simulation @@ -417,7 +415,7 @@ func TestGaiaImportExport(t *testing.T) { newDB.Close() os.RemoveAll(newDir) }() - newApp := NewGaiaApp(log.NewNopLogger(), newDB, nil, true, fauxMerkleModeOpt) + newApp := NewGaiaApp(log.NewNopLogger(), newDB, nil, true, false, fauxMerkleModeOpt) require.Equal(t, "GaiaApp", newApp.Name()) var genesisState GenesisState err = app.cdc.UnmarshalJSON(appState, &genesisState) @@ -480,7 +478,7 @@ func TestGaiaSimulationAfterImport(t *testing.T) { db.Close() os.RemoveAll(dir) }() - app := NewGaiaApp(logger, db, nil, true, fauxMerkleModeOpt) + app := NewGaiaApp(logger, db, nil, true, false, fauxMerkleModeOpt) require.Equal(t, "GaiaApp", app.Name()) // Run randomized simulation @@ -516,7 +514,7 @@ func TestGaiaSimulationAfterImport(t *testing.T) { newDB.Close() os.RemoveAll(newDir) }() - newApp := NewGaiaApp(log.NewNopLogger(), newDB, nil, true, fauxMerkleModeOpt) + newApp := NewGaiaApp(log.NewNopLogger(), newDB, nil, true, false, fauxMerkleModeOpt) require.Equal(t, "GaiaApp", newApp.Name()) newApp.InitChain(abci.RequestInitChain{ AppStateBytes: appState, @@ -544,7 +542,7 @@ func TestAppStateDeterminism(t *testing.T) { for j := 0; j < numTimesToRunPerSeed; j++ { logger := log.NewNopLogger() db := dbm.NewMemDB() - app := NewGaiaApp(logger, db, nil, true) + app := NewGaiaApp(logger, db, nil, true, false) // Run randomized simulation simulation.SimulateFromSeed( diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 8bdd645e9079..10a1692196bf 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -35,6 +35,7 @@ import ( authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" + crisisclient "github.com/cosmos/cosmos-sdk/x/crisis/client" distcmd "github.com/cosmos/cosmos-sdk/x/distribution" distClient "github.com/cosmos/cosmos-sdk/x/distribution/client" govClient "github.com/cosmos/cosmos-sdk/x/gov/client" @@ -69,6 +70,7 @@ func main() { distClient.NewModuleClient(distcmd.StoreKey, cdc), stakingClient.NewModuleClient(st.StoreKey, cdc), slashingClient.NewModuleClient(sl.StoreKey, cdc), + crisisclient.NewModuleClient(sl.StoreKey, cdc), } rootCmd := &cobra.Command{ @@ -124,7 +126,10 @@ func queryCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { ) for _, m := range mc { - queryCmd.AddCommand(m.GetQueryCmd()) + mQueryCmd := m.GetQueryCmd() + if mQueryCmd != nil { + queryCmd.AddCommand(mQueryCmd) + } } return queryCmd diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index d6d88e0dfa30..4aa8e49179f5 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -22,6 +22,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// gaiad custom flags +const flagAssertInvariantsBlockly = "assert-invariants-blockly" + +var assertInvariantsBlockly bool + func main() { cdc := app.MakeCodec() @@ -50,6 +55,8 @@ func main() { // prepare and add flags executor := cli.PrepareBaseCmd(rootCmd, "GA", app.DefaultNodeHome) + rootCmd.Flags().BoolVar(&assertInvariantsBlockly, flagAssertInvariantsBlockly, + false, "Assert registered invariants on a blockly basis") err := executor.Execute() if err != nil { // handle with #870 @@ -59,7 +66,7 @@ func main() { func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { return app.NewGaiaApp( - logger, db, traceStore, true, + logger, db, traceStore, true, assertInvariantsBlockly, baseapp.SetPruning(store.NewPruningOptionsFromString(viper.GetString("pruning"))), baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), ) @@ -68,14 +75,15 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application func exportAppStateAndTMValidators( logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailWhiteList []string, ) (json.RawMessage, []tmtypes.GenesisValidator, error) { + if height != -1 { - gApp := app.NewGaiaApp(logger, db, traceStore, false) + gApp := app.NewGaiaApp(logger, db, traceStore, false, false) err := gApp.LoadHeight(height) if err != nil { return nil, nil, err } return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } - gApp := app.NewGaiaApp(logger, db, traceStore, true) + gApp := app.NewGaiaApp(logger, db, traceStore, true, false) return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } diff --git a/cmd/gaia/cmd/gaiareplay/main.go b/cmd/gaia/cmd/gaiareplay/main.go index 78cacec36763..fd2c518a44d1 100644 --- a/cmd/gaia/cmd/gaiareplay/main.go +++ b/cmd/gaia/cmd/gaiareplay/main.go @@ -107,7 +107,7 @@ func run(rootDir string) { // Application fmt.Println("Creating application") myapp := app.NewGaiaApp( - ctx.Logger, appDB, traceStoreWriter, true, + ctx.Logger, appDB, traceStoreWriter, true, true, baseapp.SetPruning(store.PruneEverything), // nothing ) diff --git a/docs/spec/crisis/01_state.md b/docs/spec/crisis/01_state.md new file mode 100644 index 000000000000..1b454ff5afde --- /dev/null +++ b/docs/spec/crisis/01_state.md @@ -0,0 +1,14 @@ +# State + +## ConstantFee + +Due to the anticipated large gas cost requirement to verify an invariant (and +potential to exceed the maximum allowable block gas limit) a constant fee is +used instead of the standard gas consumption method. The constant fee is +intended to be larger than the anticipated gas cost of running the invariant +with the standard gas consumption method. + +The ConstantFee param is held in the global params store. + + - Params: `mint/params -> amino(sdk.Coin)` + diff --git a/docs/spec/crisis/02_messages.md b/docs/spec/crisis/02_messages.md new file mode 100644 index 000000000000..db3449f1ffb7 --- /dev/null +++ b/docs/spec/crisis/02_messages.md @@ -0,0 +1,25 @@ +# Messages + +In this section we describe the processing of the crisis messages and the +corresponding updates to the state. + +## MsgVerifyInvariant + +Blockchain invariants can be checked using the `MsgVerifyInvariant` message. + +```golang +type MsgVerifyInvariant struct { + Sender sdk.AccAddress + InvariantRoute string +} +``` + +This message is expected to fail if: + - the sender does not have enough coins for the constant fee + - the invariant route is not registered + +This message checks the invariant provided, and if the invariant is broken it +panics, halting the blockchain. If the invariant is broken, the constant fee is +never deducted as the transaction is never committed to a block (equivalent to +being refunded). However, if the invariant is not broken, the constant fee will +not be refunded. diff --git a/docs/spec/crisis/03_tags.md b/docs/spec/crisis/03_tags.md new file mode 100644 index 000000000000..f3509bbdac6d --- /dev/null +++ b/docs/spec/crisis/03_tags.md @@ -0,0 +1,13 @@ +# Tags + +The crisis module emits the following events/tags: + +## Handlers + +### MsgVerifyInvariance + +| Key | Value | +|-----------|---------------------| +| action | verify_invariant | +| sender | {message-sender} | +| invariant | {invariant-route} | diff --git a/docs/spec/crisis/README.md b/docs/spec/crisis/README.md new file mode 100644 index 000000000000..b9a9ae7dc88a --- /dev/null +++ b/docs/spec/crisis/README.md @@ -0,0 +1,16 @@ +# Crisis + +## Overview + +The crisis module halts the blockchain under the circumstance that a blockchain +invariant is broken. Invariants can be registered with the application during the +application initialization process. + +## Contents + +1. **[State](01_state.md)** + - [ConstantFee](01_state.md#constantfee) +2. **[Messages](02_messages.md)** + - [MsgVerifyInvariant](02_messages.md#msgverifyinvariant) +3. **[Tags](03_tags.md)** + - [Handlers](03_tags.md#handlers) diff --git a/x/auth/stdtx.go b/x/auth/stdtx.go index 542a962e4664..5f11a2f3e7f7 100644 --- a/x/auth/stdtx.go +++ b/x/auth/stdtx.go @@ -150,6 +150,15 @@ func (fee StdFee) Bytes() []byte { return bz } +// GasPrices returns the gas prices for a StdFee. +// +// NOTE: The gas prices returned are not the true gas prices that were +// originally part of the submitted transaction because the fee is computed +// as fee = ceil(gasWanted * gasPrices). +func (fee StdFee) GasPrices() sdk.DecCoins { + return sdk.NewDecCoins(fee.Amount).QuoDec(sdk.NewDec(int64(fee.Gas))) +} + //__________________________________________________________ // StdSignDoc is replay-prevention structure. diff --git a/x/bank/expected_keepers.go b/x/bank/expected_keepers.go new file mode 100644 index 000000000000..6d256d926d6e --- /dev/null +++ b/x/bank/expected_keepers.go @@ -0,0 +1,10 @@ +package bank + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// expected crisis keeper +type CrisisKeeper interface { + RegisterRoute(moduleName, route string, invar sdk.Invariant) +} diff --git a/x/bank/simulation/invariants.go b/x/bank/invariants.go similarity index 85% rename from x/bank/simulation/invariants.go rename to x/bank/invariants.go index f6ee642e3b94..4ca4c3d9a1df 100644 --- a/x/bank/simulation/invariants.go +++ b/x/bank/invariants.go @@ -1,4 +1,4 @@ -package simulation +package bank import ( "errors" @@ -8,6 +8,12 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" ) +// register bank invariants +func RegisterInvariants(c CrisisKeeper, ak auth.AccountKeeper) { + c.RegisterRoute("bank", "nonnegative-outstanding", + NonnegativeBalanceInvariant(ak)) +} + // NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances func NonnegativeBalanceInvariant(ak auth.AccountKeeper) sdk.Invariant { return func(ctx sdk.Context) error { diff --git a/x/crisis/client/cli/tx.go b/x/crisis/client/cli/tx.go new file mode 100644 index 000000000000..d44759ea4509 --- /dev/null +++ b/x/crisis/client/cli/tx.go @@ -0,0 +1,33 @@ +// nolint +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + "github.com/cosmos/cosmos-sdk/x/crisis" +) + +// command to replace a delegator's withdrawal address +func GetCmdInvariantBroken(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "invariant-broken [module-name] [invariant-route]", + Short: "submit proof that an invariant broken to halt the chain", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + + txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc) + + senderAddr := cliCtx.GetFromAddress() + moduleName, route := args[0], args[1] + msg := crisis.NewMsgVerifyInvariant(senderAddr, moduleName, route) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false) + }, + } + return cmd +} diff --git a/x/crisis/client/module_client.go b/x/crisis/client/module_client.go new file mode 100644 index 000000000000..12c9d2282b49 --- /dev/null +++ b/x/crisis/client/module_client.go @@ -0,0 +1,42 @@ +package client + +import ( + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/x/crisis" + "github.com/cosmos/cosmos-sdk/x/crisis/client/cli" +) + +// ModuleClient exports all client functionality from this module +type ModuleClient struct { + storeKey string + cdc *amino.Codec +} + +// NewModuleClient creates a new ModuleClient object +func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient { + return ModuleClient{ + storeKey: storeKey, + cdc: cdc, + } +} + +// GetQueryCmd returns the cli query commands for this module +func (ModuleClient) GetQueryCmd() *cobra.Command { + return nil +} + +// GetTxCmd returns the transaction commands for this module +func (mc ModuleClient) GetTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: crisis.ModuleName, + Short: "crisis transactions subcommands", + } + + txCmd.AddCommand(client.PostCommands( + cli.GetCmdInvariantBroken(mc.cdc), + )...) + return txCmd +} diff --git a/x/crisis/codec.go b/x/crisis/codec.go new file mode 100644 index 000000000000..d5217676444f --- /dev/null +++ b/x/crisis/codec.go @@ -0,0 +1,20 @@ +package crisis + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// Register concrete types on codec codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgVerifyInvariant{}, "cosmos-sdk/MsgVerifyInvariant", nil) +} + +// generic sealed codec to be used throughout module +var MsgCdc *codec.Codec + +func init() { + cdc := codec.New() + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + MsgCdc = cdc.Seal() +} diff --git a/x/crisis/errors.go b/x/crisis/errors.go new file mode 100644 index 000000000000..7a03134fd582 --- /dev/null +++ b/x/crisis/errors.go @@ -0,0 +1,23 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // default codespace for crisis module + DefaultCodespace sdk.CodespaceType = ModuleName + + // CodeInvalidInput is the codetype for invalid input for the crisis module + CodeInvalidInput sdk.CodeType = 103 +) + +// ErrNilSender - no sender provided for the input +func ErrNilSender(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "sender address is nil") +} + +// ErrUnknownInvariant - unknown invariant provided +func ErrUnknownInvariant(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "unknown invariant") +} diff --git a/x/crisis/expected_keepers.go b/x/crisis/expected_keepers.go new file mode 100644 index 000000000000..e620bdd3aa49 --- /dev/null +++ b/x/crisis/expected_keepers.go @@ -0,0 +1,20 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// expected bank keeper +type DistrKeeper interface { + DistributeFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) sdk.Error +} + +// expected fee collection keeper +type FeeCollectionKeeper interface { + AddCollectedFees(ctx sdk.Context, coins sdk.Coins) sdk.Coins +} + +// expected bank keeper +type BankKeeper interface { + SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) +} diff --git a/x/crisis/genesis.go b/x/crisis/genesis.go new file mode 100644 index 000000000000..7cd702b8486b --- /dev/null +++ b/x/crisis/genesis.go @@ -0,0 +1,40 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GenesisState - crisis genesis state +type GenesisState struct { + ConstantFee sdk.Coin `json:"constant_fee"` +} + +// NewGenesisState creates a new GenesisState object +func NewGenesisState(constantFee sdk.Coin) GenesisState { + return GenesisState{ + ConstantFee: constantFee, + } +} + +// DefaultGenesisState creates a default GenesisState object +func DefaultGenesisState() GenesisState { + return GenesisState{ + ConstantFee: sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000)), + } +} + +// new crisis genesis +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + keeper.SetConstantFee(ctx, data.ConstantFee) +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + constantFee := keeper.GetConstantFee(ctx) + return NewGenesisState(constantFee) +} + +// ValidateGenesis - placeholder function +func ValidateGenesis(data GenesisState) error { + return nil +} diff --git a/x/crisis/handler.go b/x/crisis/handler.go new file mode 100644 index 000000000000..ae753804d1bd --- /dev/null +++ b/x/crisis/handler.go @@ -0,0 +1,80 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ModuleName is the module name for this module +const ( + ModuleName = "crisis" + RouterKey = ModuleName +) + +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + + switch msg := msg.(type) { + case MsgVerifyInvariant: + return handleMsgVerifyInvariant(ctx, msg, k) + default: + return sdk.ErrTxDecode("invalid message parse in crisis module").Result() + } + } +} + +func handleMsgVerifyInvariant(ctx sdk.Context, msg MsgVerifyInvariant, k Keeper) sdk.Result { + + // remove the constant fee + constantFee := sdk.NewCoins(k.GetConstantFee(ctx)) + _, _, err := k.bankKeeper.SubtractCoins(ctx, msg.Sender, constantFee) + if err != nil { + return err.Result() + } + _ = k.feeCollectionKeeper.AddCollectedFees(ctx, constantFee) + + // use a cached context to avoid gas costs during invariants + cacheCtx, _ := ctx.CacheContext() + + found := false + var invarianceErr error + msgFullRoute := msg.FullInvariantRoute() + for _, invarRoute := range k.routes { + if invarRoute.FullRoute() == msgFullRoute { + invarianceErr = invarRoute.Invar(cacheCtx) + found = true + break + } + } + if !found { + return ErrUnknownInvariant(DefaultCodespace).Result() + } + + if invarianceErr != nil { + + // NOTE currently, because the chain halts here, this transaction will never be included + // in the blockchain thus the constant fee will have never been deducted. Thus no + // refund is required. + + // TODO uncomment the following code block with implementation of the circuit breaker + //// refund constant fee + //err := k.distrKeeper.DistributeFeePool(ctx, constantFee, msg.Sender) + //if err != nil { + //// if there are insufficient coins to refund, log the error, + //// but still halt the chain. + //logger := ctx.Logger().With("module", "x/crisis") + //logger.Error(fmt.Sprintf( + //"WARNING: insufficient funds to allocate to sender from fee pool, err: %s", err)) + //} + + // TODO replace with circuit breaker + panic(invarianceErr) + } + + tags := sdk.NewTags( + "sender", msg.Sender.String(), + "invariant", msg.InvariantRoute, + ) + return sdk.Result{ + Tags: tags, + } +} diff --git a/x/crisis/handler_test.go b/x/crisis/handler_test.go new file mode 100644 index 000000000000..1f0aa6d147c4 --- /dev/null +++ b/x/crisis/handler_test.go @@ -0,0 +1,100 @@ +package crisis + +import ( + "errors" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/stretchr/testify/require" +) + +var ( + testModuleName = "dummy" + dummyRouteWhichPasses = NewInvarRoute(testModuleName, "which-passes", func(_ sdk.Context) error { return nil }) + dummyRouteWhichFails = NewInvarRoute(testModuleName, "which-fails", func(_ sdk.Context) error { return errors.New("whoops") }) + addrs = distr.TestAddrs +) + +func CreateTestInput(t *testing.T) (sdk.Context, Keeper, auth.AccountKeeper, distr.Keeper) { + + communityTax := sdk.NewDecWithPrec(2, 2) + ctx, accKeeper, bankKeeper, distrKeeper, _, feeCollectionKeeper, paramsKeeper := + distr.CreateTestInputAdvanced(t, false, 10, communityTax) + + paramSpace := paramsKeeper.Subspace(DefaultParamspace) + crisisKeeper := NewKeeper(paramSpace, distrKeeper, bankKeeper, feeCollectionKeeper) + constantFee := sdk.NewInt64Coin("stake", 10000000) + crisisKeeper.SetConstantFee(ctx, constantFee) + + crisisKeeper.RegisterRoute(testModuleName, dummyRouteWhichPasses.Route, dummyRouteWhichPasses.Invar) + crisisKeeper.RegisterRoute(testModuleName, dummyRouteWhichFails.Route, dummyRouteWhichFails.Invar) + + // set the community pool to pay back the constant fee + feePool := distr.InitialFeePool() + feePool.CommunityPool = sdk.NewDecCoins(sdk.NewCoins(constantFee)) + distrKeeper.SetFeePool(ctx, feePool) + + return ctx, crisisKeeper, accKeeper, distrKeeper +} + +//____________________________________________________________________________ + +func TestHandleMsgVerifyInvariantWithNotEnoughSenderCoins(t *testing.T) { + ctx, crisisKeeper, accKeeper, _ := CreateTestInput(t) + sender := addrs[0] + coin := accKeeper.GetAccount(ctx, sender).GetCoins()[0] + excessCoins := sdk.NewCoin(coin.Denom, coin.Amount.AddRaw(1)) + crisisKeeper.SetConstantFee(ctx, excessCoins) + + msg := NewMsgVerifyInvariant(sender, testModuleName, dummyRouteWhichPasses.Route) + res := handleMsgVerifyInvariant(ctx, msg, crisisKeeper) + require.False(t, res.IsOK()) +} + +func TestHandleMsgVerifyInvariantWithBadInvariant(t *testing.T) { + ctx, crisisKeeper, _, _ := CreateTestInput(t) + sender := addrs[0] + + msg := NewMsgVerifyInvariant(sender, testModuleName, "route-that-doesnt-exist") + res := handleMsgVerifyInvariant(ctx, msg, crisisKeeper) + require.False(t, res.IsOK()) +} + +func TestHandleMsgVerifyInvariantWithInvariantBroken(t *testing.T) { + ctx, crisisKeeper, _, _ := CreateTestInput(t) + sender := addrs[0] + + msg := NewMsgVerifyInvariant(sender, testModuleName, dummyRouteWhichFails.Route) + var res sdk.Result + require.Panics(t, func() { + res = handleMsgVerifyInvariant(ctx, msg, crisisKeeper) + }, fmt.Sprintf("%v", res)) +} + +func TestHandleMsgVerifyInvariantWithInvariantBrokenAndNotEnoughPoolCoins(t *testing.T) { + ctx, crisisKeeper, _, distrKeeper := CreateTestInput(t) + sender := addrs[0] + + // set the community pool to empty + feePool := distrKeeper.GetFeePool(ctx) + feePool.CommunityPool = sdk.DecCoins{} + distrKeeper.SetFeePool(ctx, feePool) + + msg := NewMsgVerifyInvariant(sender, testModuleName, dummyRouteWhichFails.Route) + var res sdk.Result + require.Panics(t, func() { + res = handleMsgVerifyInvariant(ctx, msg, crisisKeeper) + }, fmt.Sprintf("%v", res)) +} + +func TestHandleMsgVerifyInvariantWithInvariantNotBroken(t *testing.T) { + ctx, crisisKeeper, _, _ := CreateTestInput(t) + sender := addrs[0] + + msg := NewMsgVerifyInvariant(sender, testModuleName, dummyRouteWhichPasses.Route) + res := handleMsgVerifyInvariant(ctx, msg, crisisKeeper) + require.True(t, res.IsOK()) +} diff --git a/x/crisis/keeper.go b/x/crisis/keeper.go new file mode 100644 index 000000000000..8501b08ee072 --- /dev/null +++ b/x/crisis/keeper.go @@ -0,0 +1,41 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Keeper - crisis keeper +type Keeper struct { + routes []InvarRoute + paramSpace params.Subspace + + distrKeeper DistrKeeper + bankKeeper BankKeeper + feeCollectionKeeper FeeCollectionKeeper +} + +// NewKeeper creates a new Keeper object +func NewKeeper(paramSpace params.Subspace, + distrKeeper DistrKeeper, bankKeeper BankKeeper, + feeCollectionKeeper FeeCollectionKeeper) Keeper { + + return Keeper{ + routes: []InvarRoute{}, + paramSpace: paramSpace.WithKeyTable(ParamKeyTable()), + distrKeeper: distrKeeper, + bankKeeper: bankKeeper, + feeCollectionKeeper: feeCollectionKeeper, + } +} + +// register routes for the +func (k *Keeper) RegisterRoute(moduleName, route string, invar sdk.Invariant) { + invarRoute := NewInvarRoute(moduleName, route, invar) + k.routes = append(k.routes, invarRoute) +} + +// Routes - return the keeper's invariant routes +func (k Keeper) Routes() []InvarRoute { + return k.routes +} diff --git a/x/crisis/msg.go b/x/crisis/msg.go new file mode 100644 index 000000000000..d6289d2b4181 --- /dev/null +++ b/x/crisis/msg.go @@ -0,0 +1,52 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MsgVerifyInvariant - message struct to verify a particular invariance +type MsgVerifyInvariant struct { + Sender sdk.AccAddress `json:"sender"` + InvariantModuleName string `json:"invariant_module_name"` + InvariantRoute string `json:"invariant_route"` +} + +// ensure Msg interface compliance at compile time +var _ sdk.Msg = &MsgVerifyInvariant{} + +// NewMsgVerifyInvariant creates a new MsgVerifyInvariant object +func NewMsgVerifyInvariant(sender sdk.AccAddress, invariantModuleName, + invariantRoute string) MsgVerifyInvariant { + + return MsgVerifyInvariant{ + Sender: sender, + InvariantModuleName: invariantModuleName, + InvariantRoute: invariantRoute, + } +} + +//nolint +func (msg MsgVerifyInvariant) Route() string { return ModuleName } +func (msg MsgVerifyInvariant) Type() string { return "verify_invariant" } + +// get the bytes for the message signer to sign on +func (msg MsgVerifyInvariant) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } + +// GetSignBytes gets the sign bytes for the msg MsgVerifyInvariant +func (msg MsgVerifyInvariant) GetSignBytes() []byte { + bz := MsgCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// quick validity check +func (msg MsgVerifyInvariant) ValidateBasic() sdk.Error { + if msg.Sender.Empty() { + return ErrNilSender(DefaultCodespace) + } + return nil +} + +// FullInvariantRoute - get the messages full invariant route +func (msg MsgVerifyInvariant) FullInvariantRoute() string { + return msg.InvariantModuleName + "/" + msg.InvariantRoute +} diff --git a/x/crisis/params.go b/x/crisis/params.go new file mode 100644 index 000000000000..14c8a2567315 --- /dev/null +++ b/x/crisis/params.go @@ -0,0 +1,34 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Default parameter namespace +const ( + DefaultParamspace = ModuleName +) + +var ( + // key for constant fee parameter + ParamStoreKeyConstantFee = []byte("ConstantFee") +) + +// type declaration for parameters +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable( + ParamStoreKeyConstantFee, sdk.Coin{}, + ) +} + +// GetConstantFee get's the constant fee from the paramSpace +func (k Keeper) GetConstantFee(ctx sdk.Context) (constantFee sdk.Coin) { + k.paramSpace.Get(ctx, ParamStoreKeyConstantFee, &constantFee) + return +} + +// GetConstantFee set's the constant fee in the paramSpace +func (k Keeper) SetConstantFee(ctx sdk.Context, constantFee sdk.Coin) { + k.paramSpace.Set(ctx, ParamStoreKeyConstantFee, constantFee) +} diff --git a/x/crisis/route.go b/x/crisis/route.go new file mode 100644 index 000000000000..0f8808581c26 --- /dev/null +++ b/x/crisis/route.go @@ -0,0 +1,26 @@ +package crisis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// invariant route +type InvarRoute struct { + ModuleName string + Route string + Invar sdk.Invariant +} + +// NewInvarRoute - create an InvarRoute object +func NewInvarRoute(moduleName, route string, invar sdk.Invariant) InvarRoute { + return InvarRoute{ + ModuleName: moduleName, + Route: route, + Invar: invar, + } +} + +// get the full invariance route +func (i InvarRoute) FullRoute() string { + return i.ModuleName + "/" + i.Route +} diff --git a/x/distribution/alias.go b/x/distribution/alias.go index 34ad7dd57604..616991a47a1b 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -59,6 +59,14 @@ var ( NewQueryDelegatorParams = keeper.NewQueryDelegatorParams NewQueryDelegatorWithdrawAddrParams = keeper.NewQueryDelegatorWithdrawAddrParams DefaultParamspace = keeper.DefaultParamspace + RegisterInvariants = keeper.RegisterInvariants + AllInvariants = keeper.AllInvariants + NonNegativeOutstandingInvariant = keeper.NonNegativeOutstandingInvariant + CanWithdrawInvariant = keeper.CanWithdrawInvariant + ReferenceCountInvariant = keeper.ReferenceCountInvariant + CreateTestInputDefault = keeper.CreateTestInputDefault + CreateTestInputAdvanced = keeper.CreateTestInputAdvanced + TestAddrs = keeper.TestAddrs RegisterCodec = types.RegisterCodec DefaultGenesisState = types.DefaultGenesisState diff --git a/x/distribution/keeper/allocation_test.go b/x/distribution/keeper/allocation_test.go index 8bbab7aef20a..956121d581d5 100644 --- a/x/distribution/keeper/allocation_test.go +++ b/x/distribution/keeper/allocation_test.go @@ -105,7 +105,7 @@ func TestAllocateTokensToManyValidators(t *testing.T) { func TestAllocateTokensTruncation(t *testing.T) { communityTax := sdk.NewDec(0) - ctx, _, k, sk, fck := CreateTestInputAdvanced(t, false, 1000000, communityTax) + ctx, _, _, k, sk, fck, _ := CreateTestInputAdvanced(t, false, 1000000, communityTax) sh := staking.NewHandler(sk) // create validator with 10% commission diff --git a/x/distribution/keeper/fee_pool.go b/x/distribution/keeper/fee_pool.go new file mode 100644 index 000000000000..d8e6998db5e4 --- /dev/null +++ b/x/distribution/keeper/fee_pool.go @@ -0,0 +1,25 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +// DistributeFeePool distributes funds from the the community pool to a receiver address +func (k Keeper) DistributeFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) sdk.Error { + feePool := k.GetFeePool(ctx) + + poolTruncated, _ := feePool.CommunityPool.TruncateDecimal() + if !poolTruncated.IsAllGTE(amount) { + return types.ErrBadDistribution(k.codespace) + } + + feePool.CommunityPool.Sub(sdk.NewDecCoins(amount)) + _, _, err := k.bankKeeper.AddCoins(ctx, receiveAddr, amount) + if err != nil { + return err + } + + k.SetFeePool(ctx, feePool) + return nil +} diff --git a/x/distribution/simulation/invariants.go b/x/distribution/keeper/invariants.go similarity index 78% rename from x/distribution/simulation/invariants.go rename to x/distribution/keeper/invariants.go index 853e9494b932..4eb2a540054a 100644 --- a/x/distribution/simulation/invariants.go +++ b/x/distribution/keeper/invariants.go @@ -1,25 +1,34 @@ -package simulation +package keeper import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - distr "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/distribution/types" ) +// register all distribution invariants +func RegisterInvariants(c types.CrisisKeeper, k Keeper, stk types.StakingKeeper) { + c.RegisterRoute(types.ModuleName, "nonnegative-outstanding", + NonNegativeOutstandingInvariant(k)) + c.RegisterRoute(types.ModuleName, "can-withdraw", + CanWithdrawInvariant(k, stk)) + c.RegisterRoute(types.ModuleName, "reference-count", + ReferenceCountInvariant(k, stk)) +} + // AllInvariants runs all invariants of the distribution module -func AllInvariants(d distr.Keeper, stk types.StakingKeeper) sdk.Invariant { +func AllInvariants(k Keeper, stk types.StakingKeeper) sdk.Invariant { return func(ctx sdk.Context) error { - err := CanWithdrawInvariant(d, stk)(ctx) + err := CanWithdrawInvariant(k, stk)(ctx) if err != nil { return err } - err = NonNegativeOutstandingInvariant(d)(ctx) + err = NonNegativeOutstandingInvariant(k)(ctx) if err != nil { return err } - err = ReferenceCountInvariant(d, stk)(ctx) + err = ReferenceCountInvariant(k, stk)(ctx) if err != nil { return err } @@ -28,7 +37,7 @@ func AllInvariants(d distr.Keeper, stk types.StakingKeeper) sdk.Invariant { } // NonNegativeOutstandingInvariant checks that outstanding unwithdrawn fees are never negative -func NonNegativeOutstandingInvariant(k distr.Keeper) sdk.Invariant { +func NonNegativeOutstandingInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) error { var outstanding sdk.DecCoins @@ -51,7 +60,7 @@ func NonNegativeOutstandingInvariant(k distr.Keeper) sdk.Invariant { } // CanWithdrawInvariant checks that current rewards can be completely withdrawn -func CanWithdrawInvariant(k distr.Keeper, sk types.StakingKeeper) sdk.Invariant { +func CanWithdrawInvariant(k Keeper, sk types.StakingKeeper) sdk.Invariant { return func(ctx sdk.Context) error { // cache, we don't want to write changes @@ -95,7 +104,7 @@ func CanWithdrawInvariant(k distr.Keeper, sk types.StakingKeeper) sdk.Invariant } // ReferenceCountInvariant checks that the number of historical rewards records is correct -func ReferenceCountInvariant(k distr.Keeper, sk types.StakingKeeper) sdk.Invariant { +func ReferenceCountInvariant(k Keeper, sk types.StakingKeeper) sdk.Invariant { return func(ctx sdk.Context) error { valCount := uint64(0) diff --git a/x/distribution/keeper/key.go b/x/distribution/keeper/key.go index 068e05b857ec..c11b9536837e 100644 --- a/x/distribution/keeper/key.go +++ b/x/distribution/keeper/key.go @@ -4,11 +4,12 @@ import ( "encoding/binary" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" ) const ( // default paramspace for params keeper - DefaultParamspace = "distr" + DefaultParamspace = types.ModuleName ) // keys diff --git a/x/distribution/keeper/test_common.go b/x/distribution/keeper/test_common.go index 094f16316317..032cd3dfa33b 100644 --- a/x/distribution/keeper/test_common.go +++ b/x/distribution/keeper/test_common.go @@ -47,7 +47,8 @@ var ( valConsAddr2 = sdk.ConsAddress(valConsPk2.Address()) valConsAddr3 = sdk.ConsAddress(valConsPk3.Address()) - addrs = []sdk.AccAddress{ + // test addresses + TestAddrs = []sdk.AccAddress{ delAddr1, delAddr2, delAddr3, valAccAddr1, valAccAddr2, valAccAddr3, } @@ -75,13 +76,15 @@ func CreateTestInputDefault(t *testing.T, isCheckTx bool, initPower int64) ( sdk.Context, auth.AccountKeeper, Keeper, staking.Keeper, DummyFeeCollectionKeeper) { communityTax := sdk.NewDecWithPrec(2, 2) - return CreateTestInputAdvanced(t, isCheckTx, initPower, communityTax) + + ctx, ak, _, dk, sk, fck, _ := CreateTestInputAdvanced(t, isCheckTx, initPower, communityTax) + return ctx, ak, dk, sk, fck } // hogpodge of all sorts of input required for testing func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64, - communityTax sdk.Dec) ( - sdk.Context, auth.AccountKeeper, Keeper, staking.Keeper, DummyFeeCollectionKeeper) { + communityTax sdk.Dec) (sdk.Context, auth.AccountKeeper, bank.Keeper, + Keeper, staking.Keeper, DummyFeeCollectionKeeper, params.Keeper) { initCoins := sdk.TokensFromTendermintPower(initPower) @@ -112,15 +115,15 @@ func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64, ctx := sdk.NewContext(ms, abci.Header{ChainID: "foochainid"}, isCheckTx, log.NewNopLogger()) accountKeeper := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) - ck := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) - sk := staking.NewKeeper(cdc, keyStaking, tkeyStaking, ck, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace) + bankKeeper := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) + sk := staking.NewKeeper(cdc, keyStaking, tkeyStaking, bankKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace) sk.SetPool(ctx, staking.InitialPool()) sk.SetParams(ctx, staking.DefaultParams()) // fill all the addresses with some coins, set the loose pool tokens simultaneously - for _, addr := range addrs { + for _, addr := range TestAddrs { pool := sk.GetPool(ctx) - _, _, err := ck.AddCoins(ctx, addr, sdk.Coins{ + _, _, err := bankKeeper.AddCoins(ctx, addr, sdk.Coins{ sdk.NewCoin(sk.GetParams(ctx).BondDenom, initCoins), }) require.Nil(t, err) @@ -129,7 +132,7 @@ func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64, } fck := DummyFeeCollectionKeeper{} - keeper := NewKeeper(cdc, keyDistr, pk.Subspace(DefaultParamspace), ck, sk, fck, types.DefaultCodespace) + keeper := NewKeeper(cdc, keyDistr, pk.Subspace(DefaultParamspace), bankKeeper, sk, fck, types.DefaultCodespace) // set the distribution hooks on staking sk.SetHooks(keeper.Hooks()) @@ -140,7 +143,7 @@ func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initPower int64, keeper.SetBaseProposerReward(ctx, sdk.NewDecWithPrec(1, 2)) keeper.SetBonusProposerReward(ctx, sdk.NewDecWithPrec(4, 2)) - return ctx, accountKeeper, keeper, sk, fck + return ctx, accountKeeper, bankKeeper, keeper, sk, fck, pk } //__________________________________________________________________________________ @@ -151,6 +154,10 @@ var heldFees sdk.Coins var _ types.FeeCollectionKeeper = DummyFeeCollectionKeeper{} // nolint +func (fck DummyFeeCollectionKeeper) AddCollectedFees(_ sdk.Context, in sdk.Coins) sdk.Coins { + fck.SetCollectedFees(heldFees.Add(in)) + return heldFees +} func (fck DummyFeeCollectionKeeper) GetCollectedFees(_ sdk.Context) sdk.Coins { return heldFees } diff --git a/x/distribution/types/errors.go b/x/distribution/types/errors.go index 274cb441920c..dfe988ff34a1 100644 --- a/x/distribution/types/errors.go +++ b/x/distribution/types/errors.go @@ -36,3 +36,6 @@ func ErrNoValidatorCommission(codespace sdk.CodespaceType) sdk.Error { func ErrSetWithdrawAddrDisabled(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeSetWithdrawAddrDisabled, "set withdraw address disabled") } +func ErrBadDistribution(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInput, "community pool does not have sufficient coins to distribute") +} diff --git a/x/distribution/types/expected_keepers.go b/x/distribution/types/expected_keepers.go index eef0daa547b2..4c09613290be 100644 --- a/x/distribution/types/expected_keepers.go +++ b/x/distribution/types/expected_keepers.go @@ -28,3 +28,8 @@ type FeeCollectionKeeper interface { GetCollectedFees(ctx sdk.Context) sdk.Coins ClearCollectedFees(ctx sdk.Context) } + +// expected crisis keeper +type CrisisKeeper interface { + RegisterRoute(moduleName, route string, invar sdk.Invariant) +} diff --git a/x/distribution/types/keys.go b/x/distribution/types/keys.go index 7bd90425d71d..21d182acbb5c 100644 --- a/x/distribution/types/keys.go +++ b/x/distribution/types/keys.go @@ -1,15 +1,18 @@ package types const ( + // ModuleName is the module name constant used in many places + ModuleName = "distr" + // StoreKey is the store key string for distribution - StoreKey = "distr" + StoreKey = ModuleName // TStoreKey is the transient store key for distribution - TStoreKey = "transient_distr" + TStoreKey = "transient_" + ModuleName // RouterKey is the message route for distribution - RouterKey = "distr" + RouterKey = ModuleName // QuerierRoute is the querier route for distribution - QuerierRoute = "distr" + QuerierRoute = ModuleName ) diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index 30f170f03358..2f773d4732a4 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -5,9 +5,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// name to identify transaction types -const MsgRoute = "distr" - // Verify interface at compile time var _, _, _ sdk.Msg = &MsgSetWithdrawAddress{}, &MsgWithdrawDelegatorReward{}, &MsgWithdrawValidatorCommission{} @@ -24,7 +21,7 @@ func NewMsgSetWithdrawAddress(delAddr, withdrawAddr sdk.AccAddress) MsgSetWithdr } } -func (msg MsgSetWithdrawAddress) Route() string { return MsgRoute } +func (msg MsgSetWithdrawAddress) Route() string { return ModuleName } func (msg MsgSetWithdrawAddress) Type() string { return "set_withdraw_address" } // Return address that must sign over msg.GetSignBytes() @@ -62,7 +59,7 @@ func NewMsgWithdrawDelegatorReward(delAddr sdk.AccAddress, valAddr sdk.ValAddres } } -func (msg MsgWithdrawDelegatorReward) Route() string { return MsgRoute } +func (msg MsgWithdrawDelegatorReward) Route() string { return ModuleName } func (msg MsgWithdrawDelegatorReward) Type() string { return "withdraw_delegator_reward" } // Return address that must sign over msg.GetSignBytes() @@ -98,7 +95,7 @@ func NewMsgWithdrawValidatorCommission(valAddr sdk.ValAddress) MsgWithdrawValida } } -func (msg MsgWithdrawValidatorCommission) Route() string { return MsgRoute } +func (msg MsgWithdrawValidatorCommission) Route() string { return ModuleName } func (msg MsgWithdrawValidatorCommission) Type() string { return "withdraw_validator_rewards_all" } // Return address that must sign over msg.GetSignBytes() diff --git a/x/gov/simulation/invariants.go b/x/gov/simulation/invariants.go deleted file mode 100644 index 89f886ed71e8..000000000000 --- a/x/gov/simulation/invariants.go +++ /dev/null @@ -1,14 +0,0 @@ -package simulation - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// AllInvariants tests all governance invariants -func AllInvariants() sdk.Invariant { - return func(ctx sdk.Context) error { - // TODO Add some invariants! - // Checking proposal queues, no passed-but-unexecuted proposals, etc. - return nil - } -} diff --git a/x/mint/genesis.go b/x/mint/genesis.go index 27615ed7ee47..11f96c6f90bb 100644 --- a/x/mint/genesis.go +++ b/x/mint/genesis.go @@ -4,12 +4,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// GenesisState - all distribution state that must be provided at genesis +// GenesisState - minter state type GenesisState struct { Minter Minter `json:"minter"` // minter object Params Params `json:"params"` // inflation params } +// NewGenesisState creates a new GenesisState object func NewGenesisState(minter Minter, params Params) GenesisState { return GenesisState{ Minter: minter, @@ -17,7 +18,7 @@ func NewGenesisState(minter Minter, params Params) GenesisState { } } -// get raw genesis raw message for testing +// DefaultGenesisState creates a default GenesisState object func DefaultGenesisState() GenesisState { return GenesisState{ Minter: DefaultInitialMinter(), @@ -31,8 +32,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { keeper.SetParams(ctx, data.Params) } -// ExportGenesis returns a GenesisState for a given context and keeper. The -// GenesisState will contain the pool, and validator/delegator distribution info's +// ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { minter := keeper.GetMinter(ctx) @@ -40,8 +40,8 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { return NewGenesisState(minter, params) } -// ValidateGenesis validates the provided staking genesis state to ensure the -// expected invariants holds. (i.e. params in correct bounds, no duplicate validators) +// ValidateGenesis validates the provided genesis state to ensure the +// expected invariants holds. func ValidateGenesis(data GenesisState) error { err := validateParams(data.Params) if err != nil { diff --git a/x/slashing/simulation/invariants.go b/x/slashing/simulation/invariants.go deleted file mode 100644 index 1545db2c75db..000000000000 --- a/x/slashing/simulation/invariants.go +++ /dev/null @@ -1,13 +0,0 @@ -package simulation - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// TODO Any invariants to check here? -// AllInvariants tests all slashing invariants -func AllInvariants() sdk.Invariant { - return func(_ sdk.Context) error { - return nil - } -} diff --git a/x/staking/alias.go b/x/staking/alias.go index 999814b6d289..cee0c7b4c475 100644 --- a/x/staking/alias.go +++ b/x/staking/alias.go @@ -68,6 +68,12 @@ var ( UnbondingQueueKey = keeper.UnbondingQueueKey RedelegationQueueKey = keeper.RedelegationQueueKey ValidatorQueueKey = keeper.ValidatorQueueKey + RegisterInvariants = keeper.RegisterInvariants + AllInvariants = keeper.AllInvariants + SupplyInvariants = keeper.SupplyInvariants + NonNegativePowerInvariant = keeper.NonNegativePowerInvariant + PositiveDelegationInvariant = keeper.PositiveDelegationInvariant + DelegatorSharesInvariant = keeper.DelegatorSharesInvariant DefaultParamspace = keeper.DefaultParamspace KeyUnbondingTime = types.KeyUnbondingTime diff --git a/x/staking/simulation/invariants.go b/x/staking/keeper/invariants.go similarity index 79% rename from x/staking/simulation/invariants.go rename to x/staking/keeper/invariants.go index db6249719f3f..f9a495a4f6eb 100644 --- a/x/staking/simulation/invariants.go +++ b/x/staking/keeper/invariants.go @@ -1,4 +1,4 @@ -package simulation +package keeper import ( "bytes" @@ -6,15 +6,26 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/staking" - "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/types" ) +// register all staking invariants +func RegisterInvariants(c types.CrisisKeeper, k Keeper, f types.FeeCollectionKeeper, + d types.DistributionKeeper, am auth.AccountKeeper) { + + c.RegisterRoute(types.ModuleName, "supply", + SupplyInvariants(k, f, d, am)) + c.RegisterRoute(types.ModuleName, "nonnegative-power", + NonNegativePowerInvariant(k)) + c.RegisterRoute(types.ModuleName, "positive-delegation", + PositiveDelegationInvariant(k)) + c.RegisterRoute(types.ModuleName, "delegator-shares", + DelegatorSharesInvariant(k)) +} + // AllInvariants runs all invariants of the staking module. -// Currently: total supply, positive power -func AllInvariants(k staking.Keeper, - f staking.FeeCollectionKeeper, d staking.DistributionKeeper, - am auth.AccountKeeper) sdk.Invariant { +func AllInvariants(k Keeper, f types.FeeCollectionKeeper, + d types.DistributionKeeper, am auth.AccountKeeper) sdk.Invariant { return func(ctx sdk.Context) error { err := SupplyInvariants(k, f, d, am)(ctx) @@ -43,8 +54,9 @@ func AllInvariants(k staking.Keeper, // SupplyInvariants checks that the total supply reflects all held not-bonded tokens, bonded tokens, and unbonding delegations // nolint: unparam -func SupplyInvariants(k staking.Keeper, - f staking.FeeCollectionKeeper, d staking.DistributionKeeper, am auth.AccountKeeper) sdk.Invariant { +func SupplyInvariants(k Keeper, f types.FeeCollectionKeeper, + d types.DistributionKeeper, am auth.AccountKeeper) sdk.Invariant { + return func(ctx sdk.Context) error { pool := k.GetPool(ctx) @@ -54,7 +66,7 @@ func SupplyInvariants(k staking.Keeper, loose = loose.Add(acc.GetCoins().AmountOf(k.BondDenom(ctx)).ToDec()) return false }) - k.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) bool { + k.IterateUnbondingDelegations(ctx, func(_ int64, ubd types.UnbondingDelegation) bool { for _, entry := range ubd.Entries { loose = loose.Add(entry.Balance.ToDec()) } @@ -98,7 +110,7 @@ func SupplyInvariants(k staking.Keeper, } // NonNegativePowerInvariant checks that all stored validators have >= 0 power. -func NonNegativePowerInvariant(k staking.Keeper) sdk.Invariant { +func NonNegativePowerInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) error { iterator := k.ValidatorsPowerStoreIterator(ctx) @@ -108,7 +120,7 @@ func NonNegativePowerInvariant(k staking.Keeper) sdk.Invariant { panic(fmt.Sprintf("validator record not found for address: %X\n", iterator.Value())) } - powerKey := keeper.GetValidatorsByPowerIndexKey(validator) + powerKey := GetValidatorsByPowerIndexKey(validator) if !bytes.Equal(iterator.Key(), powerKey) { return fmt.Errorf("power store invariance:\n\tvalidator.Power: %v"+ @@ -126,7 +138,7 @@ func NonNegativePowerInvariant(k staking.Keeper) sdk.Invariant { } // PositiveDelegationInvariant checks that all stored delegations have > 0 shares. -func PositiveDelegationInvariant(k staking.Keeper) sdk.Invariant { +func PositiveDelegationInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) error { delegations := k.GetAllDelegations(ctx) for _, delegation := range delegations { @@ -145,7 +157,7 @@ func PositiveDelegationInvariant(k staking.Keeper) sdk.Invariant { // DelegatorSharesInvariant checks whether all the delegator shares which persist // in the delegator object add up to the correct total delegator shares // amount stored in each validator -func DelegatorSharesInvariant(k staking.Keeper) sdk.Invariant { +func DelegatorSharesInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) error { validators := k.GetAllValidators(ctx) for _, validator := range validators { diff --git a/x/staking/types/expected_keepers.go b/x/staking/types/expected_keepers.go index 54c9bad62287..2a2aff06046b 100644 --- a/x/staking/types/expected_keepers.go +++ b/x/staking/types/expected_keepers.go @@ -18,3 +18,8 @@ type BankKeeper interface { DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) } + +// expected crisis keeper +type CrisisKeeper interface { + RegisterRoute(moduleName, route string, invar sdk.Invariant) +}