Skip to content

Commit

Permalink
feat(invariants): add several invariants across modules (#1514)
Browse files Browse the repository at this point in the history
Co-authored-by: zale144 <[email protected]>
  • Loading branch information
danwt and zale144 authored Nov 22, 2024
1 parent 64fcbad commit 3109e57
Show file tree
Hide file tree
Showing 28 changed files with 750 additions and 201 deletions.
5 changes: 4 additions & 1 deletion app/apptesting/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ func (a *AppKeepers) InitKeepers(
appCodec,
a.keys[sequencermoduletypes.StoreKey],
a.BankKeeper,
a.AccountKeeper,
a.RollappKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)
Expand Down
1 change: 0 additions & 1 deletion ibctesting/bridging_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion ibctesting/transfers_enabled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions ibctesting/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions testutil/keeper/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -38,6 +39,7 @@ func SequencerKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
cdc,
storeKey,
nil,
&authkeeper.AccountKeeper{},
&rollappkeeper.Keeper{},
sample.AccAddress(),
)
Expand Down
4 changes: 4 additions & 0 deletions utils/uinv/doc.go
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions utils/uinv/funcs.go
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 26 additions & 0 deletions utils/uinv/funcs_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
82 changes: 45 additions & 37 deletions x/delayedack/keeper/invariants.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 4 additions & 8 deletions x/delayedack/keeper/invariants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
})
}
}
2 changes: 1 addition & 1 deletion x/delayedack/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 3109e57

Please sign in to comment.