From 3109e579353ee0c0a64b3e9e2759b55f74416a80 Mon Sep 17 00:00:00 2001 From: Daniel T <30197399+danwt@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:38:44 +0000 Subject: [PATCH] feat(invariants): add several invariants across modules (#1514) Co-authored-by: zale144 --- app/apptesting/test_helpers.go | 5 +- app/keepers/keepers.go | 1 + ibctesting/bridging_fee_test.go | 1 - ibctesting/transfers_enabled_test.go | 1 - ibctesting/utils_test.go | 5 +- testutil/keeper/sequencer.go | 2 + utils/uinv/doc.go | 4 + utils/uinv/funcs.go | 73 +++++++++ utils/uinv/funcs_test.go | 26 ++++ x/delayedack/keeper/invariants.go | 82 +++++----- x/delayedack/keeper/invariants_test.go | 12 +- x/delayedack/module.go | 2 +- x/eibc/keeper/invariants.go | 52 ++++++- x/eibc/keeper/invariants_test.go | 5 +- x/iro/keeper/invariants.go | 107 +++++++++++++ x/iro/module.go | 4 +- x/lightclient/keeper/invariants.go | 135 +++++++++++++---- x/lockup/keeper/invariants.go | 16 ++ x/rollapp/keeper/invariants.go | 95 ++++++------ x/sequencer/keeper/invariants.go | 192 +++++++++++++++++------- x/sequencer/keeper/keeper.go | 3 + x/sequencer/keeper/msg_server_create.go | 2 +- x/sequencer/types/expected_keepers.go | 3 +- x/sequencer/types/sequencer.go | 1 + x/sponsorship/keeper/invariants.go | 107 +++++++++++-- x/sponsorship/module.go | 4 +- x/sponsorship/types/types.go | 9 ++ x/streamer/keeper/invariants.go | 2 +- 28 files changed, 750 insertions(+), 201 deletions(-) create mode 100644 utils/uinv/doc.go create mode 100644 utils/uinv/funcs.go create mode 100644 utils/uinv/funcs_test.go create mode 100644 x/iro/keeper/invariants.go diff --git a/app/apptesting/test_helpers.go b/app/apptesting/test_helpers.go index 2b8959721..b01ae76a0 100644 --- a/app/apptesting/test_helpers.go +++ b/app/apptesting/test_helpers.go @@ -75,12 +75,15 @@ type SetupOptions struct { AppOpts types.AppOptions } +// Having this enabled led to some problems because some tests use intrusive methods to modify the state, whichs breaks invariants +var InvariantCheckInterval = uint(0) // disabled + func SetupTestingApp() (*app.App, app.GenesisState) { db := dbm.NewMemDB() encCdc := app.MakeEncodingConfig() params.SetAddressPrefixes() - newApp := app.New(log.NewNopLogger(), db, nil, true, map[int64]bool{}, app.DefaultNodeHome, 5, encCdc, EmptyAppOptions{}, bam.SetChainID(TestChainID)) + newApp := app.New(log.NewNopLogger(), db, nil, true, map[int64]bool{}, app.DefaultNodeHome, InvariantCheckInterval, encCdc, EmptyAppOptions{}, bam.SetChainID(TestChainID)) defaultGenesisState := app.NewDefaultGenesisState(encCdc.Codec) incentivesGenesisStateJson := defaultGenesisState[incentivestypes.ModuleName] diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 2c93e642e..52a31479a 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -365,6 +365,7 @@ func (a *AppKeepers) InitKeepers( appCodec, a.keys[sequencermoduletypes.StoreKey], a.BankKeeper, + a.AccountKeeper, a.RollappKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/ibctesting/bridging_fee_test.go b/ibctesting/bridging_fee_test.go index aba3dbf78..3c12c11eb 100644 --- a/ibctesting/bridging_fee_test.go +++ b/ibctesting/bridging_fee_test.go @@ -52,7 +52,6 @@ func (s *bridgingFeeSuite) TestNotRollappNoBridgingFee() { func (s *bridgingFeeSuite) TestBridgingFee() { path := s.newTransferPath(s.hubChain(), s.rollappChain()) s.coordinator.Setup(path) - s.fundSenderAccount() s.createRollappWithFinishedGenesis(path.EndpointA.ChannelID) s.registerSequencer() s.setRollappLightClientID(s.rollappCtx().ChainID(), path.EndpointA.ClientID) diff --git a/ibctesting/transfers_enabled_test.go b/ibctesting/transfers_enabled_test.go index f8a33c5a9..a559b7584 100644 --- a/ibctesting/transfers_enabled_test.go +++ b/ibctesting/transfers_enabled_test.go @@ -30,7 +30,6 @@ func (s *transfersEnabledSuite) SetupTest() { s.utilSuite.SetupTest() path := s.newTransferPath(s.hubChain(), s.rollappChain()) s.coordinator.Setup(path) - s.fundSenderAccount() s.createRollapp(false, nil) s.registerSequencer() s.path = path diff --git a/ibctesting/utils_test.go b/ibctesting/utils_test.go index 9037ca812..eee4ea0f6 100644 --- a/ibctesting/utils_test.go +++ b/ibctesting/utils_test.go @@ -118,10 +118,6 @@ func (s *utilSuite) SetupTest() { s.coordinator.Chains[rollappChainID()] = s.newTestChainWithSingleValidator(s.T(), s.coordinator, rollappChainID()) } -func (s *utilSuite) fundSenderAccount() { - // apptesting.FundAccount(s.hubApp(), s.hubCtx(), s.hubChain().SenderAccount.GetAddress(), sdk.NewCoins(rollapptypes.DefaultRegistrationFee)) -} - // CreateRollappWithFinishedGenesis creates a rollapp whose 'genesis' protocol is complete: // that is, they have finished all genesis transfers and their bridge is enabled. func (s *utilSuite) createRollappWithFinishedGenesis(canonicalChannelID string) { @@ -251,6 +247,7 @@ func (s *utilSuite) updateRollappState(endHeight uint64) { s.Require().NoError(err) } +// NOTE: does not use process the queue, it uses intrusive method which breaks invariants func (s *utilSuite) finalizeRollappState(index uint64, endHeight uint64) (sdk.Events, error) { rollappKeeper := s.hubApp().RollappKeeper ctx := s.hubCtx() diff --git a/testutil/keeper/sequencer.go b/testutil/keeper/sequencer.go index 077af5ca1..f6d8da832 100644 --- a/testutil/keeper/sequencer.go +++ b/testutil/keeper/sequencer.go @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" "github.com/stretchr/testify/require" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" @@ -38,6 +39,7 @@ func SequencerKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { cdc, storeKey, nil, + &authkeeper.AccountKeeper{}, &rollappkeeper.Keeper{}, sample.AccAddress(), ) diff --git a/utils/uinv/doc.go b/utils/uinv/doc.go new file mode 100644 index 000000000..10da07f94 --- /dev/null +++ b/utils/uinv/doc.go @@ -0,0 +1,4 @@ +// Packet uinv provides a simple way to define and register invariants in Cosmos SDK modules. +// Invariants should be written using normal code style, by returning errors containing context. +// Use NewErr to wrap errors in a way that they can be handled as invariant breaking. +package uinv diff --git a/utils/uinv/funcs.go b/utils/uinv/funcs.go new file mode 100644 index 000000000..6a90439c5 --- /dev/null +++ b/utils/uinv/funcs.go @@ -0,0 +1,73 @@ +package uinv + +import ( + "errors" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/gerr-cosmos/gerrc" +) + +var ErrBroken = gerrc.ErrInternal.Wrap("invariant broken") + +// Wrap an error to mark as invariant breaking. If the error is nil, it will return nil. +func Breaking(err error) error { + if err == nil { + return nil + } + return errors.Join(ErrBroken, err) +} + +// If any error that your function returns is invariant breaking, use this function to wrap +// it to reduce verbosity. +func AnyErrorIsBreaking(f Func) Func { + return func(ctx sdk.Context) error { + return Breaking(f(ctx)) + } +} + +// Should return an ErrBroken if invariant is broken. Other errors are logged. +type Func = func(sdk.Context) error + +type NamedFunc[K any] struct { + Name string + Func func(K) Func +} + +func (nf NamedFunc[K]) Exec(ctx sdk.Context, module string, keeper K) (string, bool) { + err := nf.Func(keeper)(ctx) + broken := false + var msg string + if err != nil { + broken = errorsmod.IsOf(err, ErrBroken) + if !broken { + ctx.Logger().Error("Invariant function error but not breaking.", "module", module, "name", nf.Name, "error", err) + // Note that if it is broken the SDK wil take care of logging the error somewhere else + } + msg = sdk.FormatInvariant(module, nf.Name, err.Error()) + } + return msg, broken +} + +type NamedFuncsList[K any] []NamedFunc[K] + +func (l NamedFuncsList[K]) RegisterInvariants(module string, ir sdk.InvariantRegistry, keeper K) { + for _, f := range l { + ir.RegisterRoute(module, f.Name, func(ctx sdk.Context) (string, bool) { + return f.Exec(ctx, module, keeper) + }) + } +} + +// Should be called in a function func AllInvariants(k Keeper) sdk.Invariant within your own module namespace. +func (l NamedFuncsList[K]) All(module string, keeper K) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + for _, invar := range l { + s, stop := invar.Exec(ctx, module, keeper) + if stop { + return s, stop + } + } + return "", false + } +} diff --git a/utils/uinv/funcs_test.go b/utils/uinv/funcs_test.go new file mode 100644 index 000000000..2d89ff2ae --- /dev/null +++ b/utils/uinv/funcs_test.go @@ -0,0 +1,26 @@ +package uinv + +import ( + "errors" + "testing" + + errorsmod "cosmossdk.io/errors" + "github.com/stretchr/testify/require" +) + +func TestSanityCheckErrorTypes(t *testing.T) { + baseErr := errors.New("base") + var nilErr error + + t.Run("breaking", func(t *testing.T) { + require.True(t, errorsmod.IsOf(Breaking(baseErr), ErrBroken)) + require.False(t, errorsmod.IsOf(Breaking(nilErr), ErrBroken)) + }) + + t.Run("join", func(t *testing.T) { + joinedBase := errors.Join(baseErr, baseErr) + joinedNil := errors.Join(nil, nil) + require.True(t, errorsmod.IsOf(Breaking(joinedBase), ErrBroken)) + require.False(t, errorsmod.IsOf(Breaking(joinedNil), ErrBroken)) + }) +} diff --git a/x/delayedack/keeper/invariants.go b/x/delayedack/keeper/invariants.go index 66e615249..f0b2fef91 100644 --- a/x/delayedack/keeper/invariants.go +++ b/x/delayedack/keeper/invariants.go @@ -1,64 +1,72 @@ package keeper import ( + "errors" "fmt" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - + "github.com/dymensionxyz/dymension/v3/utils/uinv" commontypes "github.com/dymensionxyz/dymension/v3/x/common/types" "github.com/dymensionxyz/dymension/v3/x/delayedack/types" rtypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" ) -const ( - routeFinalizedPacket = "rollapp-finalized-packet" -) +var invs = uinv.NamedFuncsList[Keeper]{ + {Name: "proof-height", Func: InvariantProofHeight}, +} -// RegisterInvariants registers the delayedack module invariants -func (k Keeper) RegisterInvariants(ir sdk.InvariantRegistry) { - // INVARIANTS DISABLED SINCE LAZY FINALIZATION FEATURE +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + invs.RegisterInvariants(types.ModuleName, ir, k) } -// PacketsFinalizationCorrespondsToFinalizationHeight checks that all rollapp packets stored are set to -// finalized status for all heights up to the latest height. -func PacketsFinalizationCorrespondsToFinalizationHeight(k Keeper) sdk.Invariant { - return func(ctx sdk.Context) (string, bool) { - var ( - broken bool - msg string - ) +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return invs.All(types.ModuleName, k) +} - for _, rollapp := range k.rollappKeeper.GetAllRollapps(ctx) { - msg = k.checkRollapp(ctx, rollapp) - if msg != "" { - msg += fmt.Sprintf("rollapp: %s, msg: %s\n", rollapp.RollappId, msg) - broken = true - } +// ensures packet not finalized before proof height is finalized +func InvariantProofHeight(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + for _, ra := range k.rollappKeeper.GetAllRollapps(ctx) { + err := k.checkRollapp(ctx, ra) + err = errorsmod.Wrapf(err, "rollapp: %s", ra.RollappId) + errs = append(errs, err) } - - return sdk.FormatInvariant(types.ModuleName, routeFinalizedPacket, msg), broken - } + return errors.Join(errs...) + }) } -func (k Keeper) checkRollapp(ctx sdk.Context, rollapp rtypes.Rollapp) (msg string) { - // will stay 0 if no state is found +func (k Keeper) checkRollapp(ctx sdk.Context, ra rtypes.Rollapp) error { + // will stay 0 if no state is ok // but will still check packets var latestFinalizedHeight uint64 - latestFinalizedStateIndex, found := k.rollappKeeper.GetLatestFinalizedStateIndex(ctx, rollapp.RollappId) - if !found { - return + latestFinalizedStateIndex, ok := k.rollappKeeper.GetLatestFinalizedStateIndex(ctx, ra.RollappId) + if !ok { + return nil } - latestFinalizedStateInfo := k.rollappKeeper.MustGetStateInfo(ctx, rollapp.RollappId, latestFinalizedStateIndex.Index) + latestFinalizedStateInfo := k.rollappKeeper.MustGetStateInfo(ctx, ra.RollappId, latestFinalizedStateIndex.Index) latestFinalizedHeight = latestFinalizedStateInfo.GetLatestHeight() - packets := k.ListRollappPackets(ctx, types.ByRollappID(rollapp.RollappId)) - for _, packet := range packets { - if packet.ProofHeight > latestFinalizedHeight && packet.Status == commontypes.Status_FINALIZED { - return fmt.Sprintf("rollapp packet for the height should not be in finalized status. height=%d, rollapp=%s, status=%s\n", - packet.ProofHeight, packet.RollappId, packet.Status) - } + packets := k.ListRollappPackets(ctx, types.ByRollappID(ra.RollappId)) + var errs []error + for _, p := range packets { + err := k.checkPacket(p, latestFinalizedHeight) + err = errorsmod.Wrapf(err, "packet: %s", p.RollappId) + errs = append(errs, err) + } + return errors.Join(errs...) +} + +func (k Keeper) checkPacket(p commontypes.RollappPacket, latestFinalizedHeight uint64) error { + finalizedTooEarly := latestFinalizedHeight < p.ProofHeight && p.Status == commontypes.Status_FINALIZED + if finalizedTooEarly { + return fmt.Errorf("finalized too early height=%d, rollapp=%s, status=%s\n", + p.ProofHeight, p.RollappId, p.Status, + ) } - return + return nil } diff --git a/x/delayedack/keeper/invariants_test.go b/x/delayedack/keeper/invariants_test.go index 81f0cb129..62ebe6a6b 100644 --- a/x/delayedack/keeper/invariants_test.go +++ b/x/delayedack/keeper/invariants_test.go @@ -2,16 +2,14 @@ package keeper_test import ( "github.com/cometbft/cometbft/libs/rand" + "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper" "github.com/dymensionxyz/dymension/v3/app/apptesting" commontypes "github.com/dymensionxyz/dymension/v3/x/common/types" - dakeeper "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper" rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" ) func (suite *DelayedAckTestSuite) TestInvariants() { - suite.T().Skip("skipping TestInvariants as it's not supported with lazy finalization feature") - initialHeight := int64(10) suite.Ctx = suite.Ctx.WithBlockHeight(initialHeight) @@ -82,8 +80,7 @@ func (suite *DelayedAckTestSuite) TestInvariants() { suite.Require().NoError(err) } - // check invariant - msg, fails := dakeeper.PacketsFinalizationCorrespondsToFinalizationHeight(suite.App.DelayedAckKeeper)(suite.Ctx) + msg, fails := keeper.AllInvariants(suite.App.DelayedAckKeeper)(suite.Ctx) suite.Require().False(fails, msg) } @@ -234,9 +231,8 @@ func (suite *DelayedAckTestSuite) TestRollappPacketsCasesInvariant() { suite.App.DelayedAckKeeper.SetRollappPacket(ctx, tc.packet) suite.App.DelayedAckKeeper.SetRollappPacket(ctx, tc.packet2) - // check invariant - _, isBroken := dakeeper.PacketsFinalizationCorrespondsToFinalizationHeight(suite.App.DelayedAckKeeper)(suite.Ctx) - suite.Require().Equal(tc.expectedIsBroken, isBroken) + _, fails := keeper.AllInvariants(suite.App.DelayedAckKeeper)(suite.Ctx) + suite.Require().Equal(tc.expectedIsBroken, fails) }) } } diff --git a/x/delayedack/module.go b/x/delayedack/module.go index e237d4290..e617c01d5 100644 --- a/x/delayedack/module.go +++ b/x/delayedack/module.go @@ -115,7 +115,7 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { - am.keeper.RegisterInvariants(ir) + keeper.RegisterInvariants(ir, am.keeper) } // InitGenesis performs the module's genesis initialization. It returns no validator updates. diff --git a/x/eibc/keeper/invariants.go b/x/eibc/keeper/invariants.go index 82e62edef..7d3fdbe64 100644 --- a/x/eibc/keeper/invariants.go +++ b/x/eibc/keeper/invariants.go @@ -17,6 +17,24 @@ const ( func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { ir.RegisterRoute(types.ModuleName, "demand-order-count", DemandOrderCountInvariant(k)) ir.RegisterRoute(types.ModuleName, "underlying-packet-exist", UnderlyingPacketExistInvariant(k)) + ir.RegisterRoute(types.ModuleName, "coins", CoinsInvariant(k)) +} + +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + for _, inv := range []sdk.Invariant{ + DemandOrderCountInvariant(k), + UnderlyingPacketExistInvariant(k), + CoinsInvariant(k), + } { + res, stop := inv(ctx) + if stop { + return res, stop + } + } + return "", false + } } func DemandOrderCountInvariant(k Keeper) sdk.Invariant { @@ -67,9 +85,41 @@ func UnderlyingPacketExistInvariant(k Keeper) sdk.Invariant { if err != nil { msg += fmt.Sprintf("underlying packet for demand order %s not found: %v\n", demandOrder.Id, err) broken = true - break } } return sdk.FormatInvariant(types.ModuleName, "underlying-packet-exist", msg), broken } } + +// coins (price,fee) are sensible +func CoinsInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + broken bool + msg string + ) + allDemandOrders, err := k.ListAllDemandOrders(ctx) + if err != nil { + msg += fmt.Sprintf("list all demand orders failed: %v\n", err) + broken = true + } + for _, do := range allDemandOrders { + for _, coins := range []sdk.Coins{do.Price, do.Fee} { + if len(coins) == 0 { + // This is OK, since coins will erase the zero coin and zero price/fee is allowed + continue + } + if len(coins) > 1 { + msg += fmt.Sprintf("multiple coins: %s\n", coins) + broken = true + continue + } + if coins[0].IsNegative() { + msg += fmt.Sprintf("negative coins: %s\n", coins) + broken = true + } + } + } + return sdk.FormatInvariant(types.ModuleName, "coins", msg), broken + } +} diff --git a/x/eibc/keeper/invariants_test.go b/x/eibc/keeper/invariants_test.go index 36fecff71..0e1ccc477 100644 --- a/x/eibc/keeper/invariants_test.go +++ b/x/eibc/keeper/invariants_test.go @@ -37,9 +37,8 @@ func (suite *KeeperTestSuite) TestInvariants() { suite.Require().NoError(err) } - // check invariant suite.Require().NotPanics(func() { - eibckeeper.DemandOrderCountInvariant(suite.App.EIBCKeeper)(ctx) - eibckeeper.UnderlyingPacketExistInvariant(suite.App.EIBCKeeper)(ctx) + _, broken := eibckeeper.AllInvariants(suite.App.EIBCKeeper)(ctx) + suite.False(broken) }) } diff --git a/x/iro/keeper/invariants.go b/x/iro/keeper/invariants.go new file mode 100644 index 000000000..81ffa4e67 --- /dev/null +++ b/x/iro/keeper/invariants.go @@ -0,0 +1,107 @@ +package keeper + +import ( + "errors" + "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + appparams "github.com/dymensionxyz/dymension/v3/app/params" + "github.com/dymensionxyz/dymension/v3/utils/uinv" + "github.com/dymensionxyz/dymension/v3/x/iro/types" +) + +var invs = uinv.NamedFuncsList[Keeper]{ + {Name: "plan", Func: InvariantPlan}, + {Name: "accounting", Func: InvariantAccounting}, +} + +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + invs.RegisterInvariants(types.ModuleName, ir, k) +} + +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return invs.All(types.ModuleName, k) +} + +// the plan should validate and struct level bookkeeping for tokens should be sensible +func InvariantPlan(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + plans := k.GetAllPlans(ctx, false) + + if len(plans) == 0 { + return nil + } + + lastPlanID := k.GetLastPlanId(ctx) + if lastPlanID != plans[len(plans)-1].Id { + return fmt.Errorf("last plan id mismatch: lastPlanID: %d, lastPlanInListID: %d", lastPlanID, plans[len(plans)-1].Id) + } + + var errs []error + for _, plan := range plans { + err := checkPlan(plan) + err = errorsmod.Wrapf(err, "planID: %d", plan.Id) + errs = append(errs, err) + } + if err := errors.Join(errs...); err != nil { + return errorsmod.Wrap(err, "check plans") + } + + return nil + }) +} + +func checkPlan(plan types.Plan) error { + if err := plan.ValidateBasic(); err != nil { + return fmt.Errorf("plan validate basic: planID: %d, err: %w", plan.Id, err) + } + + if plan.TotalAllocation.Amount.LT(plan.SoldAmt) { + return fmt.Errorf("total allocation less than sold amount: planID: %d, totalAllocation: %s, soldAmt: %s", plan.Id, plan.TotalAllocation.Amount, plan.SoldAmt) + } + + if plan.TotalAllocation.Amount.LT(plan.ClaimedAmt) { + return fmt.Errorf("total allocation less than claimed amount: planID: %d, totalAllocation: %s, claimedAmt: %s", plan.Id, plan.TotalAllocation.Amount, plan.ClaimedAmt) + } + + if plan.ClaimedAmt.GT(plan.SoldAmt) { + return fmt.Errorf("claimed amount greater than sold amount: planID: %d, claimedAmt: %s, soldAmt: %s", plan.Id, plan.ClaimedAmt, plan.SoldAmt) + } + return nil +} + +// the plan and module accounts should have sufficient and correct balances +func InvariantAccounting(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + plans := k.GetAllPlans(ctx, false) + var errs []error + + for _, plan := range plans { + if plan.IsSettled() { + // module should have no more IRO + iroBalance := k.BK.GetBalance(ctx, k.AK.GetModuleAddress(types.ModuleName), plan.GetIRODenom()) + if !iroBalance.IsZero() { + errs = append(errs, fmt.Errorf("iro tokens left in settled: planID: %d, balance: %s", plan.Id, iroBalance)) + } + + // plan should have no more dym + dymBalance := k.BK.GetBalance(ctx, plan.GetAddress(), appparams.BaseDenom) + if !dymBalance.IsZero() { + errs = append(errs, fmt.Errorf("dym tokens left in settled: planID: %d, balance: %s", plan.Id, dymBalance)) + } + } + + // Check if module has enough RA tokens to cover the claimable amount + claimable := plan.TotalAllocation.Amount.Sub(plan.ClaimedAmt) + moduleBal := k.BK.GetBalance(ctx, k.AK.GetModuleAddress(types.ModuleName), plan.SettledDenom) + if moduleBal.Amount.LT(claimable) { + errs = append(errs, fmt.Errorf("insufficient RA tokens: planID: %d, required: %s, available: %s", + plan.Id, claimable, moduleBal.Amount)) + } + } + + return errors.Join(errs...) + }) +} diff --git a/x/iro/module.go b/x/iro/module.go index edd1b366e..24ca125db 100644 --- a/x/iro/module.go +++ b/x/iro/module.go @@ -113,7 +113,9 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { } // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) -func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper) +} // InitGenesis performs the module's genesis initialization. It returns no validator updates. func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate { diff --git a/x/lightclient/keeper/invariants.go b/x/lightclient/keeper/invariants.go index 94804135e..ac9880fb7 100644 --- a/x/lightclient/keeper/invariants.go +++ b/x/lightclient/keeper/invariants.go @@ -1,50 +1,121 @@ package keeper import ( + "errors" + + "cosmossdk.io/collections" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + "github.com/dymensionxyz/dymension/v3/utils/uinv" + "github.com/dymensionxyz/gerr-cosmos/gerrc" "github.com/dymensionxyz/dymension/v3/x/lightclient/types" ) -// RegisterInvariants registers the lightclient module invariants +var invs = uinv.NamedFuncsList[Keeper]{ + {Name: "client-state", Func: InvariantClientState}, + {Name: "attribution", Func: InvariantAttribution}, +} + func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { - ir.RegisterRoute(types.ModuleName, "canonical-client-valid", CanonicalClientsValid(k)) + invs.RegisterInvariants(types.ModuleName, ir, k) } -// CanonicalClientsValid checks that all canonical clients have a known rollapp as their chain ID -func CanonicalClientsValid(k Keeper) sdk.Invariant { - return func(ctx sdk.Context) (string, bool) { - var ( - broken bool - msg string - ) +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return invs.All(types.ModuleName, k) +} + +// client state should match rollapp and have a consensus state +func InvariantClientState(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { clients := k.GetAllCanonicalClients(ctx) + var errs []error for _, client := range clients { - cs, found := k.ibcClientKeeper.GetClientState(ctx, client.IbcClientId) - if !found { - broken = true - msg += "client state not found for client ID " + client.IbcClientId + "\n" - } - tmCS, ok := cs.(*ibctm.ClientState) - if !ok { - broken = true - msg += "client state is not a tendermint client state for client ID " + client.IbcClientId + "\n" - } - if tmCS.ChainId != client.RollappId { - broken = true - msg += "client state chain ID does not match rollapp ID for client " + client.IbcClientId + "\n" - } - _, found = k.rollappKeeper.GetRollapp(ctx, client.RollappId) - if !found { - broken = true - msg += "rollapp not found for given rollapp ID " + client.RollappId + "\n" - } + errs = append(errs, checkClient(ctx, k, client)) } + return errors.Join(errs...) + }) +} - return sdk.FormatInvariant( - types.ModuleName, "canonical-client-valid", - msg, - ), broken +func checkClient(ctx sdk.Context, k Keeper, client types.CanonicalClient) error { + cs, ok := k.ibcClientKeeper.GetClientState(ctx, client.IbcClientId) + if !ok { + return gerrc.ErrNotFound.Wrapf("client state for client ID: %s", client.IbcClientId) + } + tmCS, ok := cs.(*ibctm.ClientState) + if !ok { + return gerrc.ErrInvalidArgument.Wrapf("client state is not a tendermint client state for client ID: %s", client.IbcClientId) + } + if tmCS.ChainId != client.RollappId { + return gerrc.ErrInvalidArgument.Wrapf("client state chain ID does not match rollapp ID for client ID: %s: expect: %s", client.IbcClientId, client.RollappId) + } + _, ok = k.rollappKeeper.GetRollapp(ctx, client.RollappId) + if !ok { + return gerrc.ErrNotFound.Wrapf("rollapp for rollapp ID: %s", client.RollappId) + } + _, ok = k.ibcClientKeeper.GetClientConsensusState(ctx, client.IbcClientId, cs.GetLatestHeight()) + if !ok { + return gerrc.ErrNotFound.Wrapf("latest consensus state for client ID: %s", client.IbcClientId) + } + return nil +} + +// the indexes used to attribute fraud should be populated and properly pruned +func InvariantAttribution(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + err := k.headerSigners.Walk(ctx, nil, func(key collections.Triple[string, string, uint64]) (bool, error) { + seq := key.K1() + clientID := key.K2() + height := key.K3() + err := checkAttributionIndexes(k, ctx, clientID, height, seq) + err = errorsmod.Wrapf(err, "header signer for sequencer: %s, client ID: %s, height: %d", seq, clientID, height) + errs = append(errs, err) + return false, nil + }) + errs = append(errs, err) + if err := errors.Join(errs...); err != nil { + return errorsmod.Wrap(err, "check header signers") + } + + errs = nil + + err = k.clientHeightToSigner.Walk(ctx, nil, func(key collections.Pair[string, uint64], seq string) (bool, error) { + clientID := key.K1() + height := key.K2() + ok, err := k.headerSigners.Has(ctx, collections.Join3(seq, clientID, height)) + if !ok || err != nil { + errs = append(errs, gerrc.ErrNotFound.Wrapf("header signer for sequencer: %s, client ID: %s, height: %d", seq, clientID, height)) + } + return false, nil + }) + errs = append(errs, err) + return errors.Join(errs...) + }) +} + +func checkAttributionIndexes(k Keeper, ctx sdk.Context, clientID string, height uint64, seq string) error { + _, err := k.clientHeightToSigner.Get(ctx, collections.Join(clientID, height)) + if err != nil { + return errorsmod.Wrapf(err, "reverse lookup for sequencer address: %s", seq) + } + _, ok := k.ibcClientKeeper.GetClientConsensusState(ctx, clientID, clienttypes.NewHeight(1, height)) + if !ok { + return gerrc.ErrNotFound.Wrapf("consensus state for client ID: %s", clientID) + } + signer, err := k.clientHeightToSigner.Get(ctx, collections.Join(clientID, height)) + if err != nil { + return errorsmod.Wrapf(err, "get signer for client ID: %s", clientID) + } + if signer != seq { + return gerrc.ErrInvalidArgument.Wrapf("signer mismatch: expected: %s, got: %s", seq, signer) + } + _, err = k.SeqK.RealSequencer(ctx, seq) + if err != nil { + return errorsmod.Wrapf(err, "get real sequencer for sequencer address: %s", seq) } + return nil } diff --git a/x/lockup/keeper/invariants.go b/x/lockup/keeper/invariants.go index 6c6abff97..ccff52cf5 100644 --- a/x/lockup/keeper/invariants.go +++ b/x/lockup/keeper/invariants.go @@ -17,6 +17,22 @@ func RegisterInvariants(ir sdk.InvariantRegistry, keeper Keeper) { ir.RegisterRoute(types.ModuleName, "locks-amount-invariant", LocksBalancesInvariant(keeper)) } +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + for _, inv := range []sdk.Invariant{ + AccumulationStoreInvariant(k), + LocksBalancesInvariant(k), + } { + res, stop := inv(ctx) + if stop { + return res, stop + } + } + return "", false + } +} + // AccumulationStoreInvariant ensures that the sum of all lockups at a given duration // is equal to the value stored within the accumulation store. func AccumulationStoreInvariant(keeper Keeper) sdk.Invariant { diff --git a/x/rollapp/keeper/invariants.go b/x/rollapp/keeper/invariants.go index c843c24cb..ee255c569 100644 --- a/x/rollapp/keeper/invariants.go +++ b/x/rollapp/keeper/invariants.go @@ -4,6 +4,7 @@ import ( "fmt" "slices" + "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" commontypes "github.com/dymensionxyz/dymension/v3/x/common/types" @@ -12,7 +13,6 @@ import ( // RegisterInvariants registers the bank module invariants func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { - ir.RegisterRoute(types.ModuleName, "rollapp-state-index", RollappLatestStateIndexInvariant(k)) ir.RegisterRoute(types.ModuleName, "rollapp-count", RollappCountInvariant(k)) ir.RegisterRoute(types.ModuleName, "block-height-to-finalization-queue", BlockHeightToFinalizationQueueInvariant(k)) ir.RegisterRoute(types.ModuleName, "rollapp-by-eip155-key", RollappByEIP155KeyInvariant(k)) @@ -23,11 +23,7 @@ func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { // AllInvariants runs all invariants of the module. func AllInvariants(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { - res, stop := RollappLatestStateIndexInvariant(k)(ctx) - if stop { - return res, stop - } - res, stop = RollappCountInvariant(k)(ctx) + res, stop := RollappCountInvariant(k)(ctx) if stop { return res, stop } @@ -69,12 +65,16 @@ func RollappByEIP155KeyInvariant(k Keeper) sdk.Invariant { continue } - _, found := k.GetRollappByEIP155(ctx, rollappID.GetEIP155ID()) + got, found := k.GetRollappByEIP155(ctx, rollappID.GetEIP155ID()) if !found { msg += fmt.Sprintf("rollapp (%s) have no eip155 key\n", rollapp.RollappId) broken = true continue } + if got.RollappId != rollapp.RollappId { + msg += fmt.Sprintf("rollapp (%s) have different rollappId\n", rollapp.RollappId) + broken = true + } } return sdk.FormatInvariant( @@ -97,8 +97,25 @@ func BlockHeightToFinalizationQueueInvariant(k Keeper) sdk.Invariant { continue } - latestStateIdx, _ := k.GetLatestStateInfoIndex(ctx, rollapp.RollappId) - latestFinalizedStateIdx, _ := k.GetLatestFinalizedStateIndex(ctx, rollapp.RollappId) + latestStateIdx, okLatest := k.GetLatestStateInfoIndex(ctx, rollapp.RollappId) + + // if not found, zero is fine, which means first expected is 1 + latestFinalizedStateIdx, okLatestFinalized := k.GetLatestFinalizedStateIndex(ctx, rollapp.RollappId) + + if !okLatest && okLatestFinalized { + msg += fmt.Sprintf("rollapp (%s) has latest finalized ix but not lastest ix\n", rollapp.RollappId) + broken = true + continue + } + + if okLatest && okLatestFinalized { + if latestStateIdx.Index < latestFinalizedStateIdx.Index { + msg += fmt.Sprintf("rollapp has latest ix < latest finalized ix: latest: %d: latest finalized: %d: rollapp: %s\n", + latestStateIdx.Index, latestFinalizedStateIdx.Index, rollapp.RollappId) + broken = true + continue + } + } firstUnfinalizedStateIdx := latestFinalizedStateIdx.Index + 1 @@ -138,6 +155,29 @@ func BlockHeightToFinalizationQueueInvariant(k Keeper) sdk.Invariant { broken = true } } + + err := k.finalizationQueue.Walk(ctx, nil, + func(key collections.Pair[uint64, string], value types.BlockHeightToFinalizationQueue) (stop bool, err error) { + if key.K2() != rollapp.RollappId { + return false, nil + } + if key.K2() != value.RollappId { + return false, fmt.Errorf("rollapp (%s) have finalizationQueue with wrong rollappId\n", rollapp.RollappId) + } + for _, idx := range value.FinalizationQueue { + if idx.Index <= latestFinalizedStateIdx.Index { + msg += fmt.Sprintf(`rollapp has index in queue which is already finalized: +latest ix: %d, latest finalized index : %d, queue ix: %d, rollapp: %s`, latestStateIdx.Index, latestFinalizedStateIdx.Index, idx.Index, rollapp.RollappId) + + broken = true + } + } + return false, nil + }) + if err != nil { + msg += fmt.Sprintf("error walking finalization queue: %s\n", err) + broken = true + } } return sdk.FormatInvariant( @@ -183,43 +223,6 @@ func RollappCountInvariant(k Keeper) sdk.Invariant { } } -// RollappLatestStateIndexInvariant checks the following invariants per each rollapp that latest state index >= finalized state index -func RollappLatestStateIndexInvariant(k Keeper) sdk.Invariant { - return func(ctx sdk.Context) (string, bool) { - var ( - broken bool - msg string - ) - - rollapps := k.GetAllRollapps(ctx) - for _, rollapp := range rollapps { - if !k.IsRollappStarted(ctx, rollapp.RollappId) { - continue - } - - latestStateIdx, found := k.GetLatestStateInfoIndex(ctx, rollapp.RollappId) - if !found { - msg += fmt.Sprintf("rollapp (%s) have no latestStateIdx\n", rollapp.RollappId) - broken = true - break - } - - latestFinalizedStateIdx, _ := k.GetLatestFinalizedStateIndex(ctx, rollapp.RollappId) - // not found is ok, it means no finalized state yet - - if latestStateIdx.Index < latestFinalizedStateIdx.Index { - msg += fmt.Sprintf("rollapp (%s) have latestStateIdx < latestFinalizedStateIdx\n", rollapp.RollappId) - broken = true - } - } - - return sdk.FormatInvariant( - types.ModuleName, "rollapp-state-index", - msg, - ), broken - } -} - // RollappFinalizedStateInvariant checks that all the states until latest finalized state are finalized func RollappFinalizedStateInvariant(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { diff --git a/x/sequencer/keeper/invariants.go b/x/sequencer/keeper/invariants.go index f36556803..ad2c267c6 100644 --- a/x/sequencer/keeper/invariants.go +++ b/x/sequencer/keeper/invariants.go @@ -1,79 +1,169 @@ package keeper import ( - sdk "github.com/cosmos/cosmos-sdk/types" + "errors" + "fmt" + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/utils/uinv" "github.com/dymensionxyz/dymension/v3/x/sequencer/types" ) +var invs = uinv.NamedFuncsList[Keeper]{ + {Name: "notice", Func: InvariantNotice}, + {Name: "hash-index", Func: InvariantProposerAddrIndex}, + {Name: "status", Func: InvariantStatus}, + {Name: "tokens", Func: InvariantTokens}, +} + // RegisterInvariants registers the sequencer module invariants func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { - ir.RegisterRoute(types.ModuleName, "sequencers-count", SequencersCountInvariant(k)) - ir.RegisterRoute(types.ModuleName, "sequencer-proposer-bonded", ProposerBondedInvariant(k)) + invs.RegisterInvariants(types.ModuleName, ir, k) } -func SequencersCountInvariant(k Keeper) sdk.Invariant { - return func(ctx sdk.Context) (string, bool) { - var ( - broken bool - msg string - ) - - sequencers := k.AllSequencers(ctx) - rollapps := k.rollappKeeper.GetAllRollapps(ctx) - - totalCount := 0 - for _, rollapp := range rollapps { - seqByRollapp := k.RollappSequencers(ctx, rollapp.RollappId) - bonded := k.RollappSequencersByStatus(ctx, rollapp.RollappId, types.Bonded) - unbonded := k.RollappSequencersByStatus(ctx, rollapp.RollappId, types.Unbonded) +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return invs.All(types.ModuleName, k) +} - if len(seqByRollapp) != len(bonded)+len(unbonded) { - broken = true - msg += "sequencer by rollapp length is not equal to sum of bonded, and unbonded " + rollapp.RollappId + "\n" +// notice queue should have only proposers who started notice +func InvariantNotice(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + seqs, err := k.NoticeQueue(ctx, nil) + if err != nil { + return err + } + var errs []error + for _, seq := range seqs { + if !seq.NoticeStarted() { + errs = append(errs, fmt.Errorf("in notice queue but notice not started: %s", seq.Address)) + } + if !k.IsProposer(ctx, seq) { + errs = append(errs, fmt.Errorf("in notice queue but not proposer: %s", seq.Address)) } - - totalCount += len(seqByRollapp) } + return errors.Join(errs...) + }) +} - if totalCount != len(sequencers) { - broken = true - msg += "total sequencer count is not equal to sum of sequencers by rollapp\n" +// the lookup proposer hash -> seq should be populated +func InvariantProposerAddrIndex(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + for _, seq := range k.AllSequencers(ctx) { + err := checkProposerAddrIndex(ctx, k, seq) + err = errorsmod.Wrapf(err, "sequencer: %s", seq.Address) + errs = append(errs, err) } + return errors.Join(errs...) + }) +} - return sdk.FormatInvariant( - types.ModuleName, "sequencers-count", - msg, - ), broken +func checkProposerAddrIndex(ctx sdk.Context, k Keeper, exp types.Sequencer) error { + hash := exp.MustProposerAddr() + got, err := k.SequencerByDymintAddr(ctx, hash) + if err != nil { + return errorsmod.Wrapf(err, "seq by dymint addr: proposer hash: %x", hash) } + if got.Address != exp.Address { + return fmt.Errorf("hash index mismatch: got addr: %s, exp addr: %s", got.Address, exp.Address) + } + return nil } -// ProposerBondedInvariant checks if the proposer and next proposer are bonded as expected -func ProposerBondedInvariant(k Keeper) sdk.Invariant { - return func(ctx sdk.Context) (string, bool) { - var ( - broken bool - msg string - ) - +// proposer and successor status' should be sensible +func InvariantStatus(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error rollapps := k.rollappKeeper.GetAllRollapps(ctx) - for _, rollapp := range rollapps { - proposer := k.GetProposer(ctx, rollapp.RollappId) - if !proposer.Bonded() { - broken = true - msg += "proposer is not bonded " + rollapp.RollappId + "\n" + for _, ra := range rollapps { + err := checkRollappStatus(ctx, k, ra.RollappId) + err = errorsmod.Wrapf(err, "rollapp: %s", ra.RollappId) + errs = append(errs, err) + } + for _, seq := range k.AllProposers(ctx) { + if !k.IsProposer(ctx, seq) { + errs = append(errs, fmt.Errorf("proposer in query is not proposer: %s", seq.Address)) } - successor := k.GetSuccessor(ctx, rollapp.RollappId) - if !successor.Bonded() { - broken = true - msg += "successor is not bonded " + rollapp.RollappId + "\n" + } + for _, seq := range k.AllSuccessors(ctx) { + if !k.IsSuccessor(ctx, seq) { + errs = append(errs, fmt.Errorf("successor in query is not successor: %s", seq.Address)) } + } + return errors.Join(errs...) + }) +} +func checkRollappStatus(ctx sdk.Context, k Keeper, ra string) error { + proposer := k.GetProposer(ctx, ra) + if !proposer.Bonded() { + return errors.New("proposer not bonded") + } + successor := k.GetSuccessor(ctx, ra) + if !successor.Bonded() { + return errors.New("successor not bonded") + } + if !proposer.Sentinel() && proposer.Address == successor.Address { + return errors.New("proposer and successor are the same") + } + if !successor.Sentinel() && proposer.Sentinel() { + return errors.New("proposer is sentinel but successor is not") + } + all := k.RollappSequencers(ctx, ra) + bonded := k.RollappSequencersByStatus(ctx, ra, types.Bonded) + unbonded := k.RollappSequencersByStatus(ctx, ra, types.Unbonded) + if len(all) != len(bonded)+len(unbonded) { + return errors.New("sequencer by rollapp length is not equal to sum of bonded, and unbonded") + } + return nil +} + +// module balance must correspond to sequencer stakes, and sequencer stakes should be sensible +func InvariantTokens(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + + for _, seq := range k.AllSequencers(ctx) { + err := checkSeqTokens(ctx, seq, k) + err = errorsmod.Wrapf(err, "sequencer: %s", seq.Address) + errs = append(errs, err) + } + + if err := errors.Join(errs...); err != nil { + return err } - return sdk.FormatInvariant( - types.ModuleName, "sequencer-bonded", - msg, - ), broken + total := sdk.NewCoin(k.bondDenom(ctx), sdk.ZeroInt()) + for _, seq := range k.AllSequencers(ctx) { + total = total.Add(seq.TokensCoin()) + } + // check module balance is equal + moduleAcc := k.accountK.GetModuleAccount(ctx, types.ModuleName) + balances := k.bankKeeper.GetAllBalances(ctx, moduleAcc.GetAddress()) + if 1 < len(balances) { + return errors.New("module account has more than one coin") + } + if !total.IsZero() && len(balances) == 0 { + return errors.New("module account has no balance") + } + if !total.IsZero() && !balances[0].IsEqual(total) { + return errors.New("module account balance not equal to sum of sequencer tokens") + } + return nil + }) +} + +func checkSeqTokens(ctx sdk.Context, seq types.Sequencer, k Keeper) error { + if err := seq.ValidateBasic(); err != nil { + return errorsmod.Wrap(err, "validate basic") + } + if err := k.validBondDenom(ctx, seq.TokensCoin()); err != nil { + return errorsmod.Wrap(err, "valid bond denom") + } + if seq.TokensCoin().Amount.IsNegative() { + return errors.New("negative seq tokens") } + return nil } diff --git a/x/sequencer/keeper/keeper.go b/x/sequencer/keeper/keeper.go index eaf3025fe..2dafc3ce4 100644 --- a/x/sequencer/keeper/keeper.go +++ b/x/sequencer/keeper/keeper.go @@ -20,6 +20,7 @@ type Keeper struct { cdc codec.BinaryCodec storeKey storetypes.StoreKey bankKeeper types.BankKeeper + accountK types.AccountKeeper rollappKeeper types.RollappKeeper unbondBlockers []UnbondBlocker hooks types.Hooks @@ -31,6 +32,7 @@ func NewKeeper( cdc codec.BinaryCodec, storeKey storetypes.StoreKey, bankKeeper types.BankKeeper, + accountK types.AccountKeeper, rollappKeeper types.RollappKeeper, authority string, ) *Keeper { @@ -46,6 +48,7 @@ func NewKeeper( storeKey: storeKey, bankKeeper: bankKeeper, rollappKeeper: rollappKeeper, + accountK: accountK, authority: authority, unbondBlockers: []UnbondBlocker{}, hooks: types.NoOpHooks{}, diff --git a/x/sequencer/keeper/msg_server_create.go b/x/sequencer/keeper/msg_server_create.go index 0fbad2032..f35afdb42 100644 --- a/x/sequencer/keeper/msg_server_create.go +++ b/x/sequencer/keeper/msg_server_create.go @@ -94,7 +94,7 @@ func (k msgServer) CreateSequencer(goCtx context.Context, msg *types.MsgCreateSe k.SetSequencer(ctx, *seq) if err := k.SetSequencerByDymintAddr(ctx, pkAddr, seq.Address); err != nil { - return nil, err + return nil, errorsmod.Wrapf(err, "set sequencer by dymint addr: %s: proposer hash: %x", seq.Address, pkAddr) } proposer := k.GetProposer(ctx, msg.RollappId) diff --git a/x/sequencer/types/expected_keepers.go b/x/sequencer/types/expected_keepers.go index cf304c8a2..d0f263c03 100644 --- a/x/sequencer/types/expected_keepers.go +++ b/x/sequencer/types/expected_keepers.go @@ -18,7 +18,7 @@ type RollappKeeper interface { // AccountKeeper defines the expected account keeper used for simulations (noalias) type AccountKeeper interface { - GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI + GetModuleAccount(ctx sdk.Context, moduleName string) types.ModuleAccountI } // BankKeeper defines the expected interface needed to retrieve account balances. @@ -26,5 +26,6 @@ type BankKeeper interface { SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error } diff --git a/x/sequencer/types/sequencer.go b/x/sequencer/types/sequencer.go index e20e56a0e..a68cebce6 100644 --- a/x/sequencer/types/sequencer.go +++ b/x/sequencer/types/sequencer.go @@ -89,6 +89,7 @@ func (seq Sequencer) NoticeStarted() bool { return seq.NoticePeriodTime != time.Time{} } +// Also called 'dymint proposer addr' in some places func (seq Sequencer) ProposerAddr() ([]byte, error) { return PubKeyAddr(seq.DymintPubKey) } diff --git a/x/sponsorship/keeper/invariants.go b/x/sponsorship/keeper/invariants.go index e818f081a..b23ce11cd 100644 --- a/x/sponsorship/keeper/invariants.go +++ b/x/sponsorship/keeper/invariants.go @@ -1,21 +1,108 @@ package keeper import ( - sdk "github.com/cosmos/cosmos-sdk/types" + "errors" + "fmt" + "cosmossdk.io/collections" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/utils/uinv" "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" ) -func RegisterInvariants(ir sdk.InvariantRegistry, keeper Keeper) { - ir.RegisterRoute(types.ModuleName, "gauge-weights", GaugeWeightsInvariant(keeper)) +var invs = uinv.NamedFuncsList[Keeper]{ + {Name: "delegator-validator-power", Func: InvariantDelegatorValidatorPower}, + {Name: "distribution", Func: InvariantDistribution}, + {Name: "votes", Func: InvariantVotes}, + {Name: "general", Func: InvariantGeneral}, +} + +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + invs.RegisterInvariants(types.ModuleName, ir, k) +} + +// DO NOT DELETE +func AllInvariants(k Keeper) sdk.Invariant { + return invs.All(types.ModuleName, k) +} + +// delegator validator power is non-negative +func InvariantDelegatorValidatorPower(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + err := k.delegatorValidatorPower.Walk(ctx, nil, + func(key collections.Pair[sdk.AccAddress, sdk.ValAddress], value math.Int) (stop bool, err error) { + if value.IsNegative() { + errs = append(errs, fmt.Errorf("negative power: %s", value)) + } + return false, nil + }) + if err != nil { + return fmt.Errorf("walk delegator validator power: %w", err) + } + return errors.Join(errs...) + }) } -func GaugeWeightsInvariant(Keeper) sdk.Invariant { - // TODO - return func(ctx sdk.Context) (string, bool) { - var broken bool - var msg string +// basic checks on voting power, and consistency with individual gauges +func InvariantDistribution(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + d, err := k.GetDistribution(ctx) + if err != nil { + return fmt.Errorf("get distribution: %w", err) + } + return d.Validate() + }) +} + +// vote weights in range and sum to not more than total (need not be 100% due to abstaining) +func InvariantVotes(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + var errs []error + err := k.IterateVotes(ctx, func(voter sdk.AccAddress, vote types.Vote) (bool, error) { + errs = append(errs, vote.Validate()) + return false, nil + }) + errs = append(errs, err) + return errors.Join(errs...) + }) +} + +// check that across data structures the total voting power and distribution is consistent +func InvariantGeneral(k Keeper) uinv.Func { + return uinv.AnyErrorIsBreaking(func(ctx sdk.Context) error { + totalVP := math.ZeroInt() + err := k.delegatorValidatorPower.Walk(ctx, nil, func(key collections.Pair[sdk.AccAddress, sdk.ValAddress], value math.Int) (stop bool, err error) { + totalVP = totalVP.Add(value) + return false, nil + }) + if err != nil { + return fmt.Errorf("sum delegator validator power: %w", err) + } + + distribution, err := k.GetDistribution(ctx) + if err != nil { + return fmt.Errorf("get distribution: %w", err) + } + + if !totalVP.Equal(distribution.VotingPower) { + return fmt.Errorf("total voting power does not equal total power in distribution: total: %s: distr: %s", totalVP, distribution.VotingPower) + } + + expectedDistribution := types.NewDistribution() + err = k.IterateVotes(ctx, func(voter sdk.AccAddress, vote types.Vote) (stop bool, err error) { + expectedDistribution = expectedDistribution.Merge(vote.ToDistribution()) + return false, nil + }) + if err != nil { + return fmt.Errorf("merge votes: %w", err) + } + + if !expectedDistribution.Equal(distribution) { + return fmt.Errorf("distribution does not match expected distribution from votes") + } - return sdk.FormatInvariant(types.ModuleName, "gauge-weights", msg), broken - } + return nil + }) } diff --git a/x/sponsorship/module.go b/x/sponsorship/module.go index e3e7cc717..5bd8bf79f 100644 --- a/x/sponsorship/module.go +++ b/x/sponsorship/module.go @@ -124,7 +124,9 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { } // RegisterInvariants registers the module's invariants. -func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper) +} // InitGenesis performs the module's genesis initialization. // Returns an empty ValidatorUpdate array. diff --git a/x/sponsorship/types/types.go b/x/sponsorship/types/types.go index 511e617af..c08ee01a8 100644 --- a/x/sponsorship/types/types.go +++ b/x/sponsorship/types/types.go @@ -15,11 +15,17 @@ func (d Distribution) Validate() error { return ErrInvalidDistribution.Wrapf("duplicated gauge id: %d", g.GaugeId) } gaugeIDs[g.GaugeId] = struct{}{} + if !g.Power.IsPositive() { // zeros are already pruned + return ErrInvalidDistribution.Wrapf("gauge power must be > 0, got %s: id: %d", g.Power, g.GaugeId) + } total = total.Add(g.Power) } if total.GT(d.VotingPower) { return ErrInvalidDistribution.Wrapf("voting power mismatch: sum of gauge powers %s is greater than the total voting power %s", total, d.VotingPower) } + if d.VotingPower.IsNegative() { + return ErrInvalidDistribution.Wrapf("voting power must be >= 0, got %s", d.VotingPower) + } return nil } @@ -28,6 +34,9 @@ func (v Vote) Validate() error { if err != nil { return ErrInvalidVote.Wrap(err.Error()) } + if !v.VotingPower.IsPositive() { + return ErrInvalidVote.Wrapf("must be > 0, got %s", v.VotingPower) + } return nil } diff --git a/x/streamer/keeper/invariants.go b/x/streamer/keeper/invariants.go index 312788c5f..7897f8108 100644 --- a/x/streamer/keeper/invariants.go +++ b/x/streamer/keeper/invariants.go @@ -16,7 +16,7 @@ func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { ir.RegisterRoute(types.ModuleName, "streams", StreamsInvariant(k)) } -// AllInvariants runs all invariants of the x/streamer module. +// DO NOT DELETE func AllInvariants(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { res, stop := LastStreamIdInvariant(k)(ctx)