refactor: remove invariants (#22994)

Co-authored-by: Marko <marko@baricevic.me>
This commit is contained in:
Alex | Skip 2024-12-20 12:13:08 -05:00 committed by GitHub
parent 6ed94527c1
commit 71ddfbddbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 10 additions and 924 deletions

View File

@ -55,6 +55,8 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i
### Deprecated
* (modules) [#22994](https://github.com/cosmos/cosmos-sdk/pull/22994) Deprecate `Invariants` and associated methods.
## [v0.52.0-rc.1](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.52.0-rc.1) - 2024-12-18
Every module contains its own CHANGELOG.md. Please refer to the module you are interested in.

View File

@ -45,7 +45,6 @@ x/{module_name}
│   ├── genesis.go
│   ├── grpc_query.go
│   ├── hooks.go
│   ├── invariants.go
│   ├── keeper.go
│   ├── keys.go
│   ├── msg_server.go

View File

@ -16,7 +16,6 @@ import (
appmodule "cosmossdk.io/core/appmodule"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
types "github.com/cosmos/cosmos-sdk/types"
module "github.com/cosmos/cosmos-sdk/types/module"
gomock "go.uber.org/mock/gomock"
)
@ -155,18 +154,6 @@ func (mr *MockAppModuleWithAllExtensionsMockRecorder) Name() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockAppModuleWithAllExtensions)(nil).Name))
}
// RegisterInvariants mocks base method.
func (m *MockAppModuleWithAllExtensions) RegisterInvariants(arg0 types.InvariantRegistry) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RegisterInvariants", arg0)
}
// RegisterInvariants indicates an expected call of RegisterInvariants.
func (mr *MockAppModuleWithAllExtensionsMockRecorder) RegisterInvariants(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInvariants", reflect.TypeOf((*MockAppModuleWithAllExtensions)(nil).RegisterInvariants), arg0)
}
// RegisterServices mocks base method.
func (m *MockAppModuleWithAllExtensions) RegisterServices(arg0 module.Configurator) {
m.ctrl.T.Helper()
@ -328,18 +315,6 @@ func (mr *MockAppModuleWithAllExtensionsABCIMockRecorder) Name() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockAppModuleWithAllExtensionsABCI)(nil).Name))
}
// RegisterInvariants mocks base method.
func (m *MockAppModuleWithAllExtensionsABCI) RegisterInvariants(arg0 types.InvariantRegistry) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RegisterInvariants", arg0)
}
// RegisterInvariants indicates an expected call of RegisterInvariants.
func (mr *MockAppModuleWithAllExtensionsABCIMockRecorder) RegisterInvariants(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInvariants", reflect.TypeOf((*MockAppModuleWithAllExtensionsABCI)(nil).RegisterInvariants), arg0)
}
// RegisterServices mocks base method.
func (m *MockAppModuleWithAllExtensionsABCI) RegisterServices(arg0 module.Configurator) {
m.ctrl.T.Helper()

View File

@ -6,17 +6,21 @@ import "fmt"
// The invariant returns a descriptive message about what happened
// and a boolean indicating whether the invariant has been broken.
// The simulator will then halt and print the logs.
// Deprecated: to be removed in > 0.52.
type Invariant func(ctx Context) (string, bool)
// Invariants defines a group of invariants
// Deprecated: to be removed in the next SDK version.
type Invariants []Invariant
// expected interface for registering invariants
// Deprecated: to be removed in the next SDK version.
type InvariantRegistry interface {
RegisterRoute(moduleName, route string, invar Invariant)
}
// FormatInvariant returns a standardized invariant message.
// Deprecated: to be removed in the next SDK version.
func FormatInvariant(module, name, msg string) string {
return fmt.Sprintf("%s: %s invariant\n%s\n", module, name, msg)
}

View File

@ -1,4 +1,4 @@
// module_test inconsistenctly import appmodulev2 & appmodulev1 due to limitation in mockgen
// module_test inconsistently imports appmodulev2 & appmodulev1 due to limitation in mockgen
// eventually, when we change mocking library, we should be consistent in our appmodule imports
package module_test
@ -14,7 +14,6 @@ import (
type AppModuleWithAllExtensions interface {
module.AppModule
module.HasServices
module.HasInvariants
appmodulev2.HasConsensusVersion
appmodulev2.HasGenesis
module.HasABCIEndBlock
@ -25,7 +24,6 @@ type AppModuleWithAllExtensionsABCI interface {
module.AppModule
module.HasServices
appmodulev2.HasABCIGenesis
module.HasInvariants
appmodulev2.HasConsensusVersion
module.HasABCIEndBlock
}

View File

@ -84,6 +84,7 @@ type HasGenesis = appmodulev2.HasGenesis
type HasABCIGenesis = appmodulev2.HasABCIGenesis
// HasInvariants is the interface for registering invariants.
// Deprecated: invariants are no longer used from modules.
type HasInvariants interface {
// RegisterInvariants registers module invariants.
RegisterInvariants(sdk.InvariantRegistry)
@ -389,6 +390,7 @@ func (m *Manager) AddQueryCommands(rootQueryCmd *cobra.Command) {
}
// RegisterInvariants registers all module invariants
// Deprecated: this function is no longer to be used as invariants are deprecated.
func (m *Manager) RegisterInvariants(ir sdk.InvariantRegistry) {
for _, module := range m.Modules {
if module, ok := module.(HasInvariants); ok {

View File

@ -104,27 +104,6 @@ func TestManagerOrderSetters(t *testing.T) {
require.Equal(t, []string{"module3", "module2", "module1"}, mm.OrderPrecommiters)
}
func TestManager_RegisterInvariants(t *testing.T) {
mockCtrl := gomock.NewController(t)
t.Cleanup(mockCtrl.Finish)
mockAppModule1 := mock.NewMockAppModuleWithAllExtensions(mockCtrl)
mockAppModule2 := mock.NewMockAppModuleWithAllExtensions(mockCtrl)
mockAppModule3 := mock.NewMockCoreAppModule(mockCtrl)
mockAppModule1.EXPECT().Name().Times(2).Return("module1")
mockAppModule2.EXPECT().Name().Times(2).Return("module2")
// TODO: This is not working for Core API modules yet
mm := module.NewManager(mockAppModule1, mockAppModule2, module.CoreAppModuleAdaptor("mockAppModule3", mockAppModule3))
require.NotNil(t, mm)
require.Equal(t, 3, len(mm.Modules))
// test RegisterInvariants
mockInvariantRegistry := mock.NewMockInvariantRegistry(mockCtrl)
mockAppModule1.EXPECT().RegisterInvariants(gomock.Eq(mockInvariantRegistry)).Times(1)
mockAppModule2.EXPECT().RegisterInvariants(gomock.Eq(mockInvariantRegistry)).Times(1)
mm.RegisterInvariants(mockInvariantRegistry)
}
func TestManager_RegisterQueryServices(t *testing.T) {
mockCtrl := gomock.NewController(t)
t.Cleanup(mockCtrl.Finish)

View File

@ -1,78 +0,0 @@
package keeper
import (
"fmt"
"cosmossdk.io/x/bank/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
)
// RegisterInvariants registers the bank module invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "nonnegative-outstanding", NonnegativeBalanceInvariant(k))
ir.RegisterRoute(types.ModuleName, "total-supply", TotalSupply(k))
}
// AllInvariants runs all invariants of the X/bank module.
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
res, stop := NonnegativeBalanceInvariant(k)(ctx)
if stop {
return res, stop
}
return TotalSupply(k)(ctx)
}
}
// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances
func NonnegativeBalanceInvariant(k ViewKeeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
count int
)
k.IterateAllBalances(ctx, func(addr sdk.AccAddress, balance sdk.Coin) bool {
if balance.IsNegative() {
count++
msg += fmt.Sprintf("\t%s has a negative balance of %s\n", addr, balance)
}
return false
})
broken := count != 0
return sdk.FormatInvariant(
types.ModuleName, "nonnegative-outstanding",
fmt.Sprintf("amount of negative balances found %d\n%s", count, msg),
), broken
}
}
// TotalSupply checks that the total supply reflects all the coins held in accounts
func TotalSupply(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
expectedTotal := sdk.Coins{}
supply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: query.PaginationMaxLimit})
if err != nil {
return sdk.FormatInvariant(types.ModuleName, "query supply",
fmt.Sprintf("error querying total supply %v", err)), false
}
k.IterateAllBalances(ctx, func(_ sdk.AccAddress, balance sdk.Coin) bool {
expectedTotal = expectedTotal.Add(balance)
return false
})
broken := !expectedTotal.Equal(supply)
return sdk.FormatInvariant(types.ModuleName, "total supply",
fmt.Sprintf(
"\tsum of accounts coins: %v\n"+
"\tsupply.Total: %v\n",
expectedTotal, supply)), broken
}
}

View File

@ -21,7 +21,6 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
@ -33,7 +32,6 @@ var (
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.AppModuleSimulation = AppModule{}
_ module.HasInvariants = AppModule{}
_ appmodule.AppModule = AppModule{}
_ appmodule.HasMigrations = AppModule{}
@ -114,11 +112,6 @@ func (am AppModule) RegisterMigrations(mr appmodule.MigrationRegistrar) error {
return nil
}
// RegisterInvariants registers the bank module invariants.
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// DefaultGenesis returns default genesis state as raw bytes for the bank module.
func (am AppModule) DefaultGenesis() json.RawMessage {
return am.cdc.MustMarshalJSON(types.DefaultGenesisState())

View File

@ -1,208 +0,0 @@
package keeper
import (
"fmt"
"cosmossdk.io/collections"
"cosmossdk.io/x/distribution/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// register all distribution invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "nonnegative-outstanding",
NonNegativeOutstandingInvariant(k))
ir.RegisterRoute(types.ModuleName, "can-withdraw",
CanWithdrawInvariant(k))
ir.RegisterRoute(types.ModuleName, "reference-count",
ReferenceCountInvariant(k))
ir.RegisterRoute(types.ModuleName, "module-account",
ModuleAccountInvariant(k))
}
// AllInvariants runs all invariants of the distribution module
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
res, stop := CanWithdrawInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = NonNegativeOutstandingInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = ReferenceCountInvariant(k)(ctx)
if stop {
return res, stop
}
return ModuleAccountInvariant(k)(ctx)
}
}
// NonNegativeOutstandingInvariant checks that outstanding unwithdrawn fees are never negative
func NonNegativeOutstandingInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var msg string
var count int
var outstanding sdk.DecCoins
err := k.ValidatorOutstandingRewards.Walk(ctx, nil, func(addr sdk.ValAddress, rewards types.ValidatorOutstandingRewards) (stop bool, err error) {
outstanding = rewards.GetRewards()
if outstanding.IsAnyNegative() {
count++
msg += fmt.Sprintf("\t%v has negative outstanding coins: %v\n", addr, outstanding)
}
return false, nil
})
if err != nil {
return sdk.FormatInvariant(types.ModuleName, "nonnegative outstanding", err.Error()), true
}
broken := count != 0
return sdk.FormatInvariant(types.ModuleName, "nonnegative outstanding",
fmt.Sprintf("found %d validators with negative outstanding rewards\n%s", count, msg)), broken
}
}
// CanWithdrawInvariant checks that current rewards can be completely withdrawn
func CanWithdrawInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
// cache, we don't want to write changes
ctx, _ = ctx.CacheContext()
var remaining sdk.DecCoins
valDelegationAddrs := make(map[string][][]byte)
allDelegations, err := k.stakingKeeper.GetAllSDKDelegations(ctx)
if err != nil {
panic(err)
}
for _, del := range allDelegations {
delAddr, err := k.addrCdc.StringToBytes(del.GetDelegatorAddr())
if err != nil {
panic(err)
}
valAddr := del.GetValidatorAddr()
valDelegationAddrs[valAddr] = append(valDelegationAddrs[valAddr], delAddr)
}
// iterate over all validators
err = k.stakingKeeper.IterateValidators(ctx, func(_ int64, val sdk.ValidatorI) (stop bool) {
valBz, err1 := k.stakingKeeper.ValidatorAddressCodec().StringToBytes(val.GetOperator())
if err != nil {
panic(err1)
}
_, _ = k.WithdrawValidatorCommission(ctx, valBz)
delegationAddrs, ok := valDelegationAddrs[val.GetOperator()]
if ok {
for _, delAddr := range delegationAddrs {
if _, err := k.WithdrawDelegationRewards(ctx, delAddr, valBz); err != nil {
panic(err)
}
}
}
var err error
remaining, err = k.GetValidatorOutstandingRewardsCoins(ctx, valBz)
if err != nil {
panic(err)
}
if len(remaining) > 0 && remaining[0].Amount.IsNegative() {
return true
}
return false
})
if err != nil {
panic(err)
}
broken := len(remaining) > 0 && remaining[0].Amount.IsNegative()
return sdk.FormatInvariant(types.ModuleName, "can withdraw",
fmt.Sprintf("remaining coins: %v\n", remaining)), broken
}
}
// ReferenceCountInvariant checks that the number of historical rewards records is correct
func ReferenceCountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
valCount := uint64(0)
err := k.stakingKeeper.IterateValidators(ctx, func(_ int64, val sdk.ValidatorI) (stop bool) {
valCount++
return false
})
if err != nil {
panic(err)
}
dels, err := k.stakingKeeper.GetAllSDKDelegations(ctx)
if err != nil {
panic(err)
}
slashCount := uint64(0)
err = k.ValidatorSlashEvents.Walk(
ctx,
nil,
func(k collections.Triple[sdk.ValAddress, uint64, uint64], event types.ValidatorSlashEvent) (stop bool, err error) {
slashCount++
return false, nil
},
)
if err != nil {
panic(err)
}
// one record per validator (last tracked period), one record per
// delegation (previous period), one record per slash (previous period)
expected := valCount + uint64(len(dels)) + slashCount
count := uint64(0)
err = k.ValidatorHistoricalRewards.Walk(
ctx, nil, func(key collections.Pair[sdk.ValAddress, uint64], rewards types.ValidatorHistoricalRewards) (stop bool, err error) {
count += uint64(rewards.ReferenceCount)
return false, nil
},
)
if err != nil {
panic(err)
}
broken := count != expected
return sdk.FormatInvariant(types.ModuleName, "reference count",
fmt.Sprintf("expected historical reference count: %d = %v validators + %v delegations + %v slashes\n"+
"total validator historical reference count: %d\n",
expected, valCount, len(dels), slashCount, count)), broken
}
}
// ModuleAccountInvariant checks that the coins held by the distr ModuleAccount
// is consistent with the sum of validator outstanding rewards
func ModuleAccountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var expectedCoins sdk.DecCoins
err := k.ValidatorOutstandingRewards.Walk(ctx, nil, func(_ sdk.ValAddress, rewards types.ValidatorOutstandingRewards) (stop bool, err error) {
expectedCoins = expectedCoins.Add(rewards.Rewards...)
return false, nil
})
if err != nil {
return sdk.FormatInvariant(types.ModuleName, "module account coins", err.Error()), true
}
expectedInt, _ := expectedCoins.TruncateDecimal()
balances := k.bankKeeper.GetAllBalances(ctx, k.GetDistributionAccount(ctx).GetAddress())
broken := !balances.Equal(expectedInt)
return sdk.FormatInvariant(
types.ModuleName, "ModuleAccount coins",
fmt.Sprintf("\texpected ModuleAccount coins: %s\n"+
"\tdistribution ModuleAccount coins: %s\n",
expectedInt, balances,
),
), broken
}
}

View File

@ -21,7 +21,6 @@ import (
sdkclient "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
@ -33,7 +32,6 @@ var (
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.AppModuleSimulation = AppModule{}
_ module.HasInvariants = AppModule{}
_ appmodule.AppModule = AppModule{}
_ appmodule.HasBeginBlocker = AppModule{}
@ -89,11 +87,6 @@ func (AppModule) RegisterInterfaces(registrar registry.InterfaceRegistrar) {
types.RegisterInterfaces(registrar)
}
// RegisterInvariants registers the distribution module invariants.
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(registrar grpc.ServiceRegistrar) error {
types.RegisterMsgServer(registrar, keeper.NewMsgServerImpl(am.keeper))

View File

@ -1,44 +0,0 @@
package keeper
import (
"fmt"
"cosmossdk.io/collections"
"cosmossdk.io/x/gov/types"
v1 "cosmossdk.io/x/gov/types/v1"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RegisterInvariants registers all governance invariants
func RegisterInvariants(ir sdk.InvariantRegistry, keeper *Keeper, bk types.BankKeeper) {
ir.RegisterRoute(types.ModuleName, "module-account", ModuleAccountInvariant(keeper, bk))
}
// ModuleAccountInvariant checks that the module account coins reflects the sum of
// deposit amounts held on store.
func ModuleAccountInvariant(keeper *Keeper, bk types.BankKeeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var expectedDeposits sdk.Coins
err := keeper.Deposits.Walk(ctx, nil, func(key collections.Pair[uint64, sdk.AccAddress], value v1.Deposit) (stop bool, err error) {
expectedDeposits = expectedDeposits.Add(value.Amount...)
return false, nil
})
if err != nil {
panic(err)
}
macc := keeper.GetGovernanceAccount(ctx)
balances := bk.GetAllBalances(ctx, macc.GetAddress())
// Require that the deposit balances are <= than the x/gov module's total
// balances. We use the <= operator since external funds can be sent to x/gov
// module's account and so the balance can be larger.
broken := !balances.IsAllGTE(expectedDeposits)
return sdk.FormatInvariant(types.ModuleName, "deposits",
fmt.Sprintf("\tgov ModuleAccount coins: %s\n\tsum of deposit amounts: %s\n",
balances, expectedDeposits)), broken
}
}

View File

@ -22,7 +22,6 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
@ -33,7 +32,6 @@ var (
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.AppModuleSimulation = AppModule{}
_ module.HasInvariants = AppModule{}
_ appmodule.AppModule = AppModule{}
_ appmodule.HasEndBlocker = AppModule{}
@ -115,11 +113,6 @@ func (AppModule) RegisterInterfaces(registrar registry.InterfaceRegistrar) {
v1beta1.RegisterInterfaces(registrar)
}
// RegisterInvariants registers module invariants
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper, am.bankKeeper)
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(registrar grpc.ServiceRegistrar) error {
msgServer := keeper.NewMsgServerImpl(am.keeper)

View File

@ -6,7 +6,6 @@ require (
cosmossdk.io/api v0.8.0-rc.3
cosmossdk.io/client/v2 v2.0.0-beta.6
cosmossdk.io/core v1.0.0-alpha.6
cosmossdk.io/core/testing v0.0.1
cosmossdk.io/depinject v1.1.0
cosmossdk.io/errors v1.0.1
cosmossdk.io/log v1.5.0
@ -33,6 +32,7 @@ require (
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.36.0-20241120201313-68e42a58b301.1 // indirect
buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.36.0-20240130113600-88ef6483f90f.1 // indirect
cosmossdk.io/collections v1.0.0-rc.1 // indirect
cosmossdk.io/core/testing v0.0.1 // indirect
cosmossdk.io/schema v1.0.0 // indirect
cosmossdk.io/x/staking v0.0.0-00010101000000-000000000000 // indirect
cosmossdk.io/x/tx v1.0.0-alpha.3 // indirect

View File

@ -1,126 +0,0 @@
package keeper
import (
"fmt"
"maps"
"math"
"slices"
storetypes "cosmossdk.io/core/store"
"cosmossdk.io/x/group"
"cosmossdk.io/x/group/errors"
groupmath "cosmossdk.io/x/group/internal/math"
"cosmossdk.io/x/group/internal/orm"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const weightInvariant = "Group-TotalWeight"
// RegisterInvariants registers all group invariants.
func RegisterInvariants(ir sdk.InvariantRegistry, keeper Keeper) {
ir.RegisterRoute(group.ModuleName, weightInvariant, GroupTotalWeightInvariant(keeper))
}
// GroupTotalWeightInvariant checks that group's TotalWeight must be equal to the sum of its members.
func GroupTotalWeightInvariant(keeper Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
msg, broken := GroupTotalWeightInvariantHelper(ctx, keeper.KVStoreService, keeper.groupTable, keeper.groupMemberByGroupIndex)
return sdk.FormatInvariant(group.ModuleName, weightInvariant, msg), broken
}
}
func GroupTotalWeightInvariantHelper(ctx sdk.Context, storeService storetypes.KVStoreService, groupTable orm.AutoUInt64Table, groupMemberByGroupIndex orm.Index) (string, bool) {
var msg string
var broken bool
kvStore := storeService.OpenKVStore(ctx)
groupIt, err := groupTable.PrefixScan(kvStore, 1, math.MaxUint64)
if err != nil {
msg += fmt.Sprintf("PrefixScan failure on group table\n%v\n", err)
return msg, broken
}
defer groupIt.Close()
groups := make(map[uint64]group.GroupInfo)
for {
var groupInfo group.GroupInfo
_, err = groupIt.LoadNext(&groupInfo)
if errors.ErrORMIteratorDone.Is(err) {
break
}
if err != nil {
msg += fmt.Sprintf("LoadNext failure on group table iterator\n%v\n", err)
return msg, broken
}
groups[groupInfo.Id] = groupInfo
}
groupByIDs := slices.Collect(maps.Keys(groups))
slices.SortFunc(groupByIDs, func(i, j uint64) int {
if groupByIDs[i] < groupByIDs[j] {
return -1
} else if groupByIDs[i] > groupByIDs[j] {
return 1
}
return 0
})
for _, groupID := range groupByIDs {
groupInfo := groups[groupID]
membersWeight, err := groupmath.NewNonNegativeDecFromString("0")
if err != nil {
msg += fmt.Sprintf("error while parsing positive dec zero for group member\n%v\n", err)
return msg, broken
}
err = func() error {
memIt, err := groupMemberByGroupIndex.Get(kvStore, groupInfo.Id)
if err != nil {
return fmt.Errorf("error while returning group member iterator for group with ID %d\n%w", groupInfo.Id, err)
}
defer memIt.Close()
for {
var groupMember group.GroupMember
_, err = memIt.LoadNext(&groupMember)
if errors.ErrORMIteratorDone.Is(err) {
break
}
if err != nil {
return fmt.Errorf("LoadNext failure on member table iterator\n%w", err)
}
curMemWeight, err := groupmath.NewPositiveDecFromString(groupMember.GetMember().GetWeight())
if err != nil {
return fmt.Errorf("error while parsing non-nengative decimal for group member %s\n%w", groupMember.Member.Address, err)
}
membersWeight, err = groupmath.Add(membersWeight, curMemWeight)
if err != nil {
return fmt.Errorf("decimal addition error while adding group member voting weight to total voting weight\n%w", err)
}
}
return nil
}()
if err != nil {
msg += err.Error() + "\n"
return msg, broken
}
groupWeight, err := groupmath.NewNonNegativeDecFromString(groupInfo.GetTotalWeight())
if err != nil {
msg += fmt.Sprintf("error while parsing non-nengative decimal for group with ID %d\n%v\n", groupInfo.Id, err)
return msg, broken
}
if groupWeight.Cmp(membersWeight) != 0 {
broken = true
msg += fmt.Sprintf("group's TotalWeight must be equal to the sum of its members' weights\ngroup weight: %s\nSum of group members weights: %s\n", groupWeight.String(), membersWeight.String())
break
}
}
return msg, broken
}

View File

@ -1,156 +0,0 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
coretesting "cosmossdk.io/core/testing"
"cosmossdk.io/log"
"cosmossdk.io/store"
"cosmossdk.io/store/metrics"
storetypes "cosmossdk.io/store/types"
"cosmossdk.io/x/group"
"cosmossdk.io/x/group/internal/orm"
"cosmossdk.io/x/group/keeper"
"github.com/cosmos/cosmos-sdk/codec"
codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil"
"github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type invariantTestSuite struct {
suite.Suite
ctx sdk.Context
cdc *codec.ProtoCodec
key *storetypes.KVStoreKey
}
func TestInvariantTestSuite(t *testing.T) {
suite.Run(t, new(invariantTestSuite))
}
func (s *invariantTestSuite) SetupSuite() {
interfaceRegistry := types.NewInterfaceRegistry()
group.RegisterInterfaces(interfaceRegistry)
cdc := codec.NewProtoCodec(interfaceRegistry)
key := storetypes.NewKVStoreKey(group.ModuleName)
db := coretesting.NewMemDB()
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics())
cms.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db)
_ = cms.LoadLatestVersion()
sdkCtx := sdk.NewContext(cms, false, log.NewNopLogger())
s.ctx = sdkCtx
s.cdc = cdc
s.key = key
}
func (s *invariantTestSuite) TestGroupTotalWeightInvariant() {
sdkCtx, _ := s.ctx.CacheContext()
curCtx, cdc, key := sdkCtx, s.cdc, s.key
addressCodec := codectestutil.CodecOptions{}.GetAddressCodec()
// Group Table
groupTable, err := orm.NewAutoUInt64Table([2]byte{keeper.GroupTablePrefix}, keeper.GroupTableSeqPrefix, &group.GroupInfo{}, cdc, addressCodec)
s.Require().NoError(err)
// Group Member Table
groupMemberTable, err := orm.NewPrimaryKeyTable([2]byte{keeper.GroupMemberTablePrefix}, &group.GroupMember{}, cdc, addressCodec)
s.Require().NoError(err)
groupMemberByGroupIndex, err := orm.NewIndex(groupMemberTable, keeper.GroupMemberByGroupIndexPrefix, func(val interface{}) ([]interface{}, error) {
group := val.(*group.GroupMember).GroupId
return []interface{}{group}, nil
}, group.GroupMember{}.GroupId)
s.Require().NoError(err)
_, _, addr1 := testdata.KeyTestPubAddr()
_, _, addr2 := testdata.KeyTestPubAddr()
addr1Str, err := addressCodec.BytesToString(addr1)
s.Require().NoError(err)
addr2Str, err := addressCodec.BytesToString(addr2)
s.Require().NoError(err)
specs := map[string]struct {
groupsInfo *group.GroupInfo
groupMembers []*group.GroupMember
expBroken bool
}{
"invariant not broken": {
groupsInfo: &group.GroupInfo{
Id: 1,
Admin: addr1Str,
Version: 1,
TotalWeight: "3",
},
groupMembers: []*group.GroupMember{
{
GroupId: 1,
Member: &group.Member{
Address: addr1Str,
Weight: "1",
},
},
{
GroupId: 1,
Member: &group.Member{
Address: addr2Str,
Weight: "2",
},
},
},
expBroken: false,
},
"group's TotalWeight must be equal to sum of its members weight ": {
groupsInfo: &group.GroupInfo{
Id: 1,
Admin: addr1Str,
Version: 1,
TotalWeight: "3",
},
groupMembers: []*group.GroupMember{
{
GroupId: 1,
Member: &group.Member{
Address: addr1Str,
Weight: "2",
},
},
{
GroupId: 1,
Member: &group.Member{
Address: addr2Str,
Weight: "2",
},
},
},
expBroken: true,
},
}
for _, spec := range specs {
cacheCurCtx, _ := curCtx.CacheContext()
groupsInfo := spec.groupsInfo
groupMembers := spec.groupMembers
storeService := runtime.NewKVStoreService(key)
kvStore := storeService.OpenKVStore(cacheCurCtx)
_, err := groupTable.Create(kvStore, groupsInfo)
s.Require().NoError(err)
for i := 0; i < len(groupMembers); i++ {
err := groupMemberTable.Create(kvStore, groupMembers[i])
s.Require().NoError(err)
}
_, broken := keeper.GroupTotalWeightInvariantHelper(cacheCurCtx, storeService, *groupTable, groupMemberByGroupIndex)
s.Require().Equal(spec.expBroken, broken)
}
}

View File

@ -20,7 +20,6 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
)
@ -32,7 +31,6 @@ var (
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.AppModuleSimulation = AppModule{}
_ module.HasInvariants = AppModule{}
_ appmodule.AppModule = AppModule{}
_ appmodule.HasEndBlocker = AppModule{}
@ -92,11 +90,6 @@ func (AppModule) RegisterLegacyAminoCodec(registrar registry.AminoRegistrar) {
group.RegisterLegacyAminoCodec(registrar)
}
// RegisterInvariants does nothing, there are no invariants to enforce
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(registrar grpc.ServiceRegistrar) error {
group.RegisterMsgServer(registrar, am.keeper)

View File

@ -1,226 +0,0 @@
package keeper
import (
"bytes"
"fmt"
"cosmossdk.io/collections"
"cosmossdk.io/math"
"cosmossdk.io/x/staking/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RegisterInvariants registers all staking invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k *Keeper) {
ir.RegisterRoute(types.ModuleName, "module-accounts",
ModuleAccountInvariants(k))
ir.RegisterRoute(types.ModuleName, "nonnegative-power",
NonNegativePowerInvariant(k))
ir.RegisterRoute(types.ModuleName, "positive-delegation",
PositiveDelegationInvariant(k))
ir.RegisterRoute(types.ModuleName, "delegator-shares",
DelegatorSharesInvariant(k))
}
// AllInvariants runs all invariants of the staking module.
func AllInvariants(k *Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
res, stop := ModuleAccountInvariants(k)(ctx)
if stop {
return res, stop
}
res, stop = NonNegativePowerInvariant(k)(ctx)
if stop {
return res, stop
}
res, stop = PositiveDelegationInvariant(k)(ctx)
if stop {
return res, stop
}
return DelegatorSharesInvariant(k)(ctx)
}
}
// ModuleAccountInvariants checks that the bonded and notBonded ModuleAccounts pools
// reflects the tokens actively bonded and not bonded
func ModuleAccountInvariants(k *Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
bonded := math.ZeroInt()
notBonded := math.ZeroInt()
bondedPool := k.GetBondedPool(ctx)
notBondedPool := k.GetNotBondedPool(ctx)
bondDenom, err := k.BondDenom(ctx)
if err != nil {
panic(err)
}
err = k.IterateValidators(ctx, func(_ int64, validator sdk.ValidatorI) bool {
switch validator.GetStatus() {
case sdk.Bonded:
bonded = bonded.Add(validator.GetTokens())
case sdk.Unbonding, sdk.Unbonded:
notBonded = notBonded.Add(validator.GetTokens())
default:
panic("invalid validator status")
}
return false
})
if err != nil {
panic(err)
}
err = k.UnbondingDelegations.Walk(
ctx,
nil,
func(key collections.Pair[[]byte, []byte], ubd types.UnbondingDelegation) (stop bool, err error) {
for _, entry := range ubd.Entries {
notBonded = notBonded.Add(entry.Balance)
}
return false, nil
},
)
if err != nil {
panic(err)
}
poolBonded := k.bankKeeper.GetBalance(ctx, bondedPool.GetAddress(), bondDenom)
poolNotBonded := k.bankKeeper.GetBalance(ctx, notBondedPool.GetAddress(), bondDenom)
broken := !poolBonded.Amount.Equal(bonded) || !poolNotBonded.Amount.Equal(notBonded)
// Bonded tokens should equal sum of tokens with bonded validators
// Not-bonded tokens should equal unbonding delegations plus tokens on unbonded validators
return sdk.FormatInvariant(types.ModuleName, "bonded and not bonded module account coins", fmt.Sprintf(
"\tPool's bonded tokens: %v\n"+
"\tsum of bonded tokens: %v\n"+
"not bonded token invariance:\n"+
"\tPool's not bonded tokens: %v\n"+
"\tsum of not bonded tokens: %v\n"+
"module accounts total (bonded + not bonded):\n"+
"\tModule Accounts' tokens: %v\n"+
"\tsum tokens: %v\n",
poolBonded, bonded, poolNotBonded, notBonded, poolBonded.Add(poolNotBonded), bonded.Add(notBonded))), broken
}
}
// NonNegativePowerInvariant checks that all stored validators have >= 0 power.
func NonNegativePowerInvariant(k *Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
broken bool
)
iterator, err := k.ValidatorsPowerStoreIterator(ctx)
if err != nil {
panic(err)
}
for ; iterator.Valid(); iterator.Next() {
validator, err := k.GetValidator(ctx, iterator.Value())
if err != nil {
panic(fmt.Sprintf("validator record not found for address: %X\n", iterator.Value()))
}
powerKey := types.GetValidatorsByPowerIndexKey(validator, k.PowerReduction(ctx), k.ValidatorAddressCodec())
if !bytes.Equal(iterator.Key(), powerKey) {
broken = true
msg += fmt.Sprintf("power store invariance:\n\tvalidator.Power: %v"+
"\n\tkey should be: %v\n\tkey in store: %v\n",
validator.GetConsensusPower(k.PowerReduction(ctx)), powerKey, iterator.Key())
}
if validator.Tokens.IsNegative() {
broken = true
msg += fmt.Sprintf("\tnegative tokens for validator: %v\n", validator)
}
}
iterator.Close()
return sdk.FormatInvariant(types.ModuleName, "nonnegative power", fmt.Sprintf("found invalid validator powers\n%s", msg)), broken
}
}
// PositiveDelegationInvariant checks that all stored delegations have > 0 shares.
func PositiveDelegationInvariant(k *Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
count int
)
delegations, err := k.GetAllDelegations(ctx)
if err != nil {
panic(err)
}
for _, delegation := range delegations {
if delegation.Shares.IsNegative() {
count++
msg += fmt.Sprintf("\tdelegation with negative shares: %+v\n", delegation)
}
if delegation.Shares.IsZero() {
count++
msg += fmt.Sprintf("\tdelegation with zero shares: %+v\n", delegation)
}
}
broken := count != 0
return sdk.FormatInvariant(types.ModuleName, "positive delegations", fmt.Sprintf(
"%d invalid delegations found\n%s", count, msg)), broken
}
}
// 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 *Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var (
msg string
broken bool
)
validators, err := k.GetAllValidators(ctx)
if err != nil {
panic(err)
}
validatorsDelegationShares := map[string]math.LegacyDec{}
// initialize a map: validator -> its delegation shares
for _, validator := range validators {
validatorsDelegationShares[validator.GetOperator()] = math.LegacyZeroDec()
}
// iterate through all the delegations to calculate the total delegation shares for each validator
delegations, err := k.GetAllDelegations(ctx)
if err != nil {
panic(err)
}
for _, delegation := range delegations {
delegationValidatorAddr := delegation.GetValidatorAddr()
validatorDelegationShares := validatorsDelegationShares[delegationValidatorAddr]
validatorsDelegationShares[delegationValidatorAddr] = validatorDelegationShares.Add(delegation.Shares)
}
// for each validator, check if its total delegation shares calculated from the step above equals to its expected delegation shares
for _, validator := range validators {
expValTotalDelShares := validator.GetDelegatorShares()
calculatedValTotalDelShares := validatorsDelegationShares[validator.GetOperator()]
if !calculatedValTotalDelShares.Equal(expValTotalDelShares) {
broken = true
msg += fmt.Sprintf("broken delegator shares invariance:\n"+
"\tvalidator.DelegatorShares: %v\n"+
"\tsum of Delegator.Shares: %v\n", expValTotalDelShares, calculatedValTotalDelShares)
}
}
return sdk.FormatInvariant(types.ModuleName, "delegator shares", msg), broken
}
}

View File

@ -20,7 +20,6 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
)
@ -32,7 +31,6 @@ var (
_ module.AppModuleSimulation = AppModule{}
_ module.HasAminoCodec = AppModule{}
_ module.HasGRPCGateway = AppModule{}
_ module.HasInvariants = AppModule{}
_ module.HasABCIGenesis = AppModule{}
_ module.HasABCIEndBlock = AppModule{}
@ -89,11 +87,6 @@ func (AppModule) GetTxCmd() *cobra.Command {
return cli.NewTxCmd()
}
// RegisterInvariants registers the staking module invariants.
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(registrar grpc.ServiceRegistrar) error {
types.RegisterMsgServer(registrar, keeper.NewMsgServerImpl(am.keeper))