diff --git a/app/ante/ante_options.go b/app/ante/ante_options.go index 3abd009b7..aee765fb2 100644 --- a/app/ante/ante_options.go +++ b/app/ante/ante_options.go @@ -6,6 +6,7 @@ import ( authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + lightclientkeeper "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" rollappkeeper "github.com/dymensionxyz/dymension/v3/x/rollapp/keeper" ethante "github.com/evmos/ethermint/app/ante" @@ -26,6 +27,7 @@ type HandlerOptions struct { MaxTxGasWanted uint64 ExtensionOptionChecker ante.ExtensionOptionChecker RollappKeeper rollappkeeper.Keeper + LightClientKeeper *lightclientkeeper.Keeper } func (options HandlerOptions) validate() error { @@ -47,5 +49,8 @@ func (options HandlerOptions) validate() error { if options.TxFeesKeeper == nil { return errorsmod.Wrap(errortypes.ErrLogic, "tx fees keeper is required for AnteHandler") } + if options.LightClientKeeper == nil { + return errorsmod.Wrap(errortypes.ErrLogic, "light client keeper is required for AnteHandler") + } return nil } diff --git a/app/ante/ante_test.go b/app/ante/ante_test.go index d0c382bd5..3b4304f90 100644 --- a/app/ante/ante_test.go +++ b/app/ante/ante_test.go @@ -56,14 +56,15 @@ func (s *AnteTestSuite) SetupTestCheckTx(isCheckTx bool) { anteHandler, err := ante.NewAnteHandler( ante.HandlerOptions{ - AccountKeeper: &s.app.AccountKeeper, - BankKeeper: s.app.BankKeeper, - IBCKeeper: s.app.IBCKeeper, - EvmKeeper: s.app.EvmKeeper, - FeeMarketKeeper: s.app.FeeMarketKeeper, - TxFeesKeeper: s.app.TxFeesKeeper, - FeegrantKeeper: s.app.FeeGrantKeeper, - SignModeHandler: txConfig.SignModeHandler(), + AccountKeeper: &s.app.AccountKeeper, + BankKeeper: s.app.BankKeeper, + IBCKeeper: s.app.IBCKeeper, + EvmKeeper: s.app.EvmKeeper, + FeeMarketKeeper: s.app.FeeMarketKeeper, + TxFeesKeeper: s.app.TxFeesKeeper, + FeegrantKeeper: s.app.FeeGrantKeeper, + SignModeHandler: txConfig.SignModeHandler(), + LightClientKeeper: &s.app.LightClientKeeper, }, ) diff --git a/app/ante/handlers.go b/app/ante/handlers.go index a1b5dacf4..444fe1b0f 100644 --- a/app/ante/handlers.go +++ b/app/ante/handlers.go @@ -12,6 +12,7 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" delayedack "github.com/dymensionxyz/dymension/v3/x/delayedack" + lightclientante "github.com/dymensionxyz/dymension/v3/x/lightclient/ante" ) func newEthAnteHandler(options HandlerOptions) sdk.AnteHandler { @@ -70,6 +71,7 @@ func newLegacyCosmosAnteHandlerEip712(options HandlerOptions) sdk.AnteHandler { // Note: signature verification uses EIP instead of the cosmos signature validator NewLegacyEip712SigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), ante.NewIncrementSequenceDecorator(options.AccountKeeper), + lightclientante.NewIBCMessagesDecorator(*options.LightClientKeeper, options.IBCKeeper.ClientKeeper, options.IBCKeeper.ChannelKeeper, options.RollappKeeper), delayedack.NewIBCProofHeightDecorator(), ibcante.NewRedundantRelayDecorator(options.IBCKeeper), ethante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper), @@ -107,6 +109,7 @@ func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler { ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), ante.NewIncrementSequenceDecorator(options.AccountKeeper), delayedack.NewIBCProofHeightDecorator(), + lightclientante.NewIBCMessagesDecorator(*options.LightClientKeeper, options.IBCKeeper.ClientKeeper, options.IBCKeeper.ChannelKeeper, options.RollappKeeper), ibcante.NewRedundantRelayDecorator(options.IBCKeeper), ethante.NewGasWantedDecorator(options.EvmKeeper, options.FeeMarketKeeper), diff --git a/app/app.go b/app/app.go index a00df4a3b..ac6638793 100644 --- a/app/app.go +++ b/app/app.go @@ -15,7 +15,6 @@ import ( simappparams "cosmossdk.io/simapp/params" "github.com/cosmos/cosmos-sdk/runtime" - "github.com/cosmos/cosmos-sdk/x/auth/posthandler" "github.com/dymensionxyz/dymension/v3/app/keepers" "github.com/dymensionxyz/dymension/v3/app/upgrades" @@ -46,6 +45,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth/posthandler" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" "github.com/cosmos/cosmos-sdk/x/crisis" @@ -217,6 +217,7 @@ func New( MaxTxGasWanted: maxGasWanted, ExtensionOptionChecker: nil, // uses default RollappKeeper: *app.RollappKeeper, + LightClientKeeper: &app.LightClientKeeper, }) if err != nil { panic(err) diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index a5b06a9ad..a42ee0d64 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -2,6 +2,7 @@ package apptesting import ( "strings" + "time" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" @@ -109,7 +110,7 @@ func (s *KeeperTestHelper) PostStateUpdate(ctx sdk.Context, rollappId, seqAddr s var bds rollapptypes.BlockDescriptors bds.BD = make([]rollapptypes.BlockDescriptor, numOfBlocks) for k := 0; k < int(numOfBlocks); k++ { - bds.BD[k] = rollapptypes.BlockDescriptor{Height: startHeight + uint64(k)} + bds.BD[k] = rollapptypes.BlockDescriptor{Height: startHeight + uint64(k), Timestamp: time.Now().UTC()} } updateState := rollapptypes.MsgUpdateState{ diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 012ad2357..3050eed8d 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -84,6 +84,8 @@ import ( eibcmoduletypes "github.com/dymensionxyz/dymension/v3/x/eibc/types" incentiveskeeper "github.com/dymensionxyz/dymension/v3/x/incentives/keeper" incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" + lightclientmodulekeeper "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + lightclientmoduletypes "github.com/dymensionxyz/dymension/v3/x/lightclient/types" rollappmodule "github.com/dymensionxyz/dymension/v3/x/rollapp" rollappmodulekeeper "github.com/dymensionxyz/dymension/v3/x/rollapp/keeper" "github.com/dymensionxyz/dymension/v3/x/rollapp/transfergenesis" @@ -143,6 +145,7 @@ type AppKeepers struct { SponsorshipKeeper sponsorshipkeeper.Keeper StreamerKeeper streamermodulekeeper.Keeper EIBCKeeper eibckeeper.Keeper + LightClientKeeper lightclientmodulekeeper.Keeper DelayedAckKeeper delayedackkeeper.Keeper DenomMetadataKeeper *denommetadatamodulekeeper.Keeper @@ -360,6 +363,14 @@ func (a *AppKeepers) InitKeepers( a.RollappKeeper, ) + a.LightClientKeeper = *lightclientmodulekeeper.NewKeeper( + appCodec, + a.keys[lightclientmoduletypes.StoreKey], + a.IBCKeeper.ClientKeeper, + a.SequencerKeeper, + a.RollappKeeper, + ) + a.RollappKeeper.SetSequencerKeeper(a.SequencerKeeper) a.IncentivesKeeper = incentiveskeeper.NewKeeper( @@ -501,7 +512,6 @@ func (a *AppKeepers) InitTransferStack() { ) a.TransferStack = a.delayedAckMiddleware a.TransferStack = transfergenesis.NewIBCModule(a.TransferStack, a.DelayedAckKeeper, *a.RollappKeeper, a.TransferKeeper, a.DenomMetadataKeeper) - a.TransferStack = transfergenesis.NewIBCModuleCanonicalChannelHack(a.TransferStack, *a.RollappKeeper, a.IBCKeeper.ChannelKeeper) // Create static IBC router, add transfer route, then set and seal it ibcRouter := ibcporttypes.NewRouter() @@ -575,6 +585,7 @@ func (a *AppKeepers) SetupHooks() { a.delayedAckMiddleware, a.StreamerKeeper.Hooks(), a.DymNSKeeper.GetRollAppHooks(), + a.LightClientKeeper.RollappHooks(), )) } diff --git a/app/keepers/keys.go b/app/keepers/keys.go index 723c3e392..c2fa25402 100644 --- a/app/keepers/keys.go +++ b/app/keepers/keys.go @@ -33,6 +33,7 @@ import ( delayedacktypes "github.com/dymensionxyz/dymension/v3/x/delayedack/types" eibcmoduletypes "github.com/dymensionxyz/dymension/v3/x/eibc/types" incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" + lightcliendmoduletypes "github.com/dymensionxyz/dymension/v3/x/lightclient/types" rollappmoduletypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" sequencermoduletypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" sponsorshiptypes "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" @@ -130,4 +131,5 @@ var KVStoreKeys = sdk.NewKVStoreKeys( poolmanagertypes.StoreKey, incentivestypes.StoreKey, txfeestypes.StoreKey, + lightcliendmoduletypes.StoreKey, ) diff --git a/app/keepers/modules.go b/app/keepers/modules.go index 6957da3d1..02037f62b 100644 --- a/app/keepers/modules.go +++ b/app/keepers/modules.go @@ -92,6 +92,8 @@ import ( incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" "github.com/dymensionxyz/dymension/v3/x/rollapp" + lightclientmodule "github.com/dymensionxyz/dymension/v3/x/lightclient" + lightclientmoduletypes "github.com/dymensionxyz/dymension/v3/x/lightclient/types" rollappmoduleclient "github.com/dymensionxyz/dymension/v3/x/rollapp/client" rollappmoduletypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" "github.com/dymensionxyz/dymension/v3/x/sequencer" @@ -150,6 +152,7 @@ var ModuleBasics = module.NewBasicManager( delayedack.AppModuleBasic{}, eibc.AppModuleBasic{}, dymnsmodule.AppModuleBasic{}, + lightclientmodule.AppModuleBasic{}, // Ethermint modules evm.AppModuleBasic{}, @@ -202,6 +205,7 @@ func (a *AppKeepers) SetupModules( denommetadatamodule.NewAppModule(a.DenomMetadataKeeper, *a.EvmKeeper, a.BankKeeper), eibcmodule.NewAppModule(appCodec, a.EIBCKeeper, a.AccountKeeper, a.BankKeeper), dymnsmodule.NewAppModule(appCodec, a.DymNSKeeper), + lightclientmodule.NewAppModule(appCodec, a.LightClientKeeper), // Ethermint app modules evm.NewAppModule(a.EvmKeeper, a.AccountKeeper, a.BankKeeper, a.GetSubspace(evmtypes.ModuleName).WithKeyTable(evmtypes.ParamKeyTable())), @@ -289,6 +293,7 @@ var BeginBlockers = []string{ incentivestypes.ModuleName, txfeestypes.ModuleName, consensusparamtypes.ModuleName, + lightclientmoduletypes.ModuleName, } var EndBlockers = []string{ @@ -328,6 +333,7 @@ var EndBlockers = []string{ incentivestypes.ModuleName, txfeestypes.ModuleName, consensusparamtypes.ModuleName, + lightclientmoduletypes.ModuleName, } var InitGenesis = []string{ @@ -367,4 +373,5 @@ var InitGenesis = []string{ incentivestypes.ModuleName, txfeestypes.ModuleName, consensusparamtypes.ModuleName, + lightclientmoduletypes.ModuleName, } diff --git a/app/upgrades/v4/constants.go b/app/upgrades/v4/constants.go index dfa8303f1..18cd32e53 100644 --- a/app/upgrades/v4/constants.go +++ b/app/upgrades/v4/constants.go @@ -5,6 +5,7 @@ import ( consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" "github.com/dymensionxyz/dymension/v3/app/upgrades" + lightclienttypes "github.com/dymensionxyz/dymension/v3/x/lightclient/types" ) const ( @@ -18,6 +19,7 @@ var Upgrade = upgrades.Upgrade{ Added: []string{ consensustypes.ModuleName, crisistypes.ModuleName, + lightclienttypes.ModuleName, }, }, } diff --git a/app/upgrades/v4/upgrade.go b/app/upgrades/v4/upgrade.go index 79ab99daf..13c85bad2 100644 --- a/app/upgrades/v4/upgrade.go +++ b/app/upgrades/v4/upgrade.go @@ -17,6 +17,8 @@ import ( slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcchannelkeeper "github.com/cosmos/ibc-go/v7/modules/core/04-channel/keeper" evmtypes "github.com/evmos/ethermint/x/evm/types" feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" @@ -26,6 +28,7 @@ import ( "github.com/dymensionxyz/dymension/v3/app/upgrades" delayedackkeeper "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper" delayedacktypes "github.com/dymensionxyz/dymension/v3/x/delayedack/types" + lightclientkeeper "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" rollappkeeper "github.com/dymensionxyz/dymension/v3/x/rollapp/keeper" rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" sequencerkeeper "github.com/dymensionxyz/dymension/v3/x/sequencer/keeper" @@ -49,6 +52,7 @@ func CreateUpgradeHandler( return nil, err } migrateSequencers(ctx, keepers.SequencerKeeper) + migrateRollappLightClients(ctx, keepers.RollappKeeper, keepers.LightClientKeeper, keepers.IBCKeeper.ChannelKeeper) // TODO: create rollapp gauges for each existing rollapp (https://github.com/dymensionxyz/dymension/issues/1005) @@ -136,6 +140,25 @@ func migrateSequencers(ctx sdk.Context, sequencerkeeper sequencerkeeper.Keeper) } } +func migrateRollappLightClients(ctx sdk.Context, rollappkeeper *rollappkeeper.Keeper, lightClientKeeper lightclientkeeper.Keeper, ibcChannelKeeper ibcchannelkeeper.Keeper) { + list := rollappkeeper.GetAllRollapps(ctx) + for _, rollapp := range list { + // check if the rollapp has a canonical channel already + if rollapp.ChannelId == "" { + return + } + // get the client ID the channel belongs to + _, connection, err := ibcChannelKeeper.GetChannelConnection(ctx, ibctransfertypes.PortID, rollapp.ChannelId) + if err != nil { + // if could not find a connection, skip the canonical client assignment + return + } + clientID := connection.GetClientID() + // store the rollapp to canonical light client ID mapping + lightClientKeeper.SetCanonicalClient(ctx, rollapp.RollappId, clientID) + } +} + func ConvertOldRollappToNew(oldRollapp rollapptypes.Rollapp) rollapptypes.Rollapp { return rollapptypes.Rollapp{ RollappId: oldRollapp.RollappId, diff --git a/go.mod b/go.mod index 70d3ab513..da45c87ad 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v7 v7.1.3 github.com/cosmos/ibc-go/v7 v7.5.1 + github.com/cosmos/ics23/go v0.10.0 github.com/decred/dcrd/dcrec/edwards v1.0.0 github.com/dustin/go-humanize v1.0.1 github.com/dymensionxyz/gerr-cosmos v1.0.0 @@ -84,7 +85,6 @@ require ( github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v0.20.1 // indirect - github.com/cosmos/ics23/go v0.10.0 // indirect github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect github.com/creachadair/taskgroup v0.4.2 // indirect diff --git a/ibctesting/genesis_transfer_test.go b/ibctesting/genesis_transfer_test.go index d403e7519..f091f44ca 100644 --- a/ibctesting/genesis_transfer_test.go +++ b/ibctesting/genesis_transfer_test.go @@ -31,10 +31,13 @@ func TestTransferGenesisTestSuite(t *testing.T) { func (s *transferGenesisSuite) SetupTest() { s.utilSuite.SetupTest() path := s.newTransferPath(s.hubChain(), s.rollappChain()) - s.coordinator.Setup(path) + s.coordinator.SetupConnections(path) s.createRollapp(false, nil) // genesis protocol is not finished yet s.registerSequencer() s.path = path + // set the canonical client before creating channels + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), rollappChainID(), s.path.EndpointA.ClientID) + s.coordinator.CreateChannels(path) // set hooks to avoid actually creating VFC contract, as this places extra requirements on the test setup // we assume that if the denom metadata was created (checked below), then the hooks ran correctly diff --git a/ibctesting/light_client_test.go b/ibctesting/light_client_test.go new file mode 100644 index 000000000..b89a2c33b --- /dev/null +++ b/ibctesting/light_client_test.go @@ -0,0 +1,350 @@ +package ibctesting_test + +import ( + "testing" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/testing/simapp" + + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + + ibctesting "github.com/cosmos/ibc-go/v7/testing" + "github.com/stretchr/testify/suite" +) + +var canonicalClientConfig = ibctesting.TendermintConfig{ + TrustLevel: types.ExpectedCanonicalClientParams.TrustLevel, + TrustingPeriod: types.ExpectedCanonicalClientParams.TrustingPeriod, + UnbondingPeriod: types.ExpectedCanonicalClientParams.UnbondingPeriod, + MaxClockDrift: types.ExpectedCanonicalClientParams.MaxClockDrift, +} + +type lightClientSuite struct { + utilSuite + path *ibctesting.Path +} + +func TestLightClientSuite(t *testing.T) { + suite.Run(t, new(lightClientSuite)) +} + +func (s *lightClientSuite) TestSetCanonicalClient_FailsTrustRequirements() { + s.createRollapp(false, nil) + s.registerSequencer() + // The default tm client does not match the trust requirements of a canonical client. + // So it should not be set as one. + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupClients(s.path) + + // Update rollapp state - this will trigger the check for prospective canonical client + currentRollappBlockHeight := uint64(s.rollappChain().App.LastBlockHeight()) + s.updateRollappState(currentRollappBlockHeight) + + _, found := s.hubApp().LightClientKeeper.GetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID) + s.False(found) +} + +func (s *lightClientSuite) TestSetCanonicalClient_FailsIncompatibleState() { + s.createRollapp(false, nil) + s.registerSequencer() + // create a custom tm client which matches the trust requirements of a canonical client + endpointA := ibctesting.NewEndpoint(s.hubChain(), &canonicalClientConfig, ibctesting.NewConnectionConfig(), ibctesting.NewChannelConfig()) + endpointB := ibctesting.NewEndpoint(s.rollappChain(), ibctesting.NewTendermintConfig(), ibctesting.NewConnectionConfig(), ibctesting.NewChannelConfig()) + endpointA.Counterparty = endpointB + endpointB.Counterparty = endpointA + s.path = &ibctesting.Path{EndpointA: endpointA, EndpointB: endpointB} + + // Creating the tm client - this will take us to the next block + s.coordinator.SetupClients(s.path) + + // Update the rollapp state - this will trigger the check for prospective canonical client + // The block descriptor root has dummy values and will not match the IBC roots for the same height + currentRollappBlockHeight := uint64(s.rollappChain().App.LastBlockHeight()) + s.updateRollappState(currentRollappBlockHeight) + + _, found := s.hubApp().LightClientKeeper.GetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID) + s.False(found) +} + +func (s *lightClientSuite) TestSetCanonicalClient_Succeeds() { + s.createRollapp(false, nil) + s.registerSequencer() + // create a custom tm client which matches the trust requirements of a canonical client + endpointA := ibctesting.NewEndpoint(s.hubChain(), &canonicalClientConfig, ibctesting.NewConnectionConfig(), ibctesting.NewChannelConfig()) + endpointB := ibctesting.NewEndpoint(s.rollappChain(), ibctesting.NewTendermintConfig(), ibctesting.NewConnectionConfig(), ibctesting.NewChannelConfig()) + endpointA.Counterparty = endpointB + endpointB.Counterparty = endpointA + s.path = &ibctesting.Path{EndpointA: endpointA, EndpointB: endpointB} + + currentHeader := s.rollappChain().CurrentHeader + startHeight := uint64(currentHeader.Height) + bd := rollapptypes.BlockDescriptor{Height: startHeight, StateRoot: currentHeader.AppHash, Timestamp: currentHeader.Time} + + // Creating the tm client - this will take us to the next block + s.NoError(s.path.EndpointA.CreateClient()) + + currentHeader = s.rollappChain().CurrentHeader + bdNext := rollapptypes.BlockDescriptor{Height: uint64(currentHeader.Height), StateRoot: currentHeader.AppHash, Timestamp: currentHeader.Time} + + // Update the rollapp state - this will trigger the check for prospective canonical client + msgUpdateState := rollapptypes.NewMsgUpdateState( + s.hubChain().SenderAccount.GetAddress().String(), + rollappChainID(), + "mock-da-path", + startHeight, + 2, + &rollapptypes.BlockDescriptors{BD: []rollapptypes.BlockDescriptor{bd, bdNext}}, + ) + _, err := s.rollappMsgServer().UpdateState(s.hubCtx(), msgUpdateState) + s.Require().NoError(err) + + canonClientID, found := s.hubApp().LightClientKeeper.GetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID) + s.True(found) + s.Equal(endpointA.ClientID, canonClientID) +} + +func (s *lightClientSuite) TestMsgUpdateClient_StateUpdateDoesntExist() { + s.createRollapp(false, nil) + s.registerSequencer() + currentRollappBlockHeight := uint64(s.rollappChain().App.LastBlockHeight()) + s.updateRollappState(currentRollappBlockHeight) + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupClients(s.path) + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID, s.path.EndpointA.ClientID) + + for i := 0; i < 10; i++ { + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + + s.NoError(s.path.EndpointA.UpdateClient()) + // As there was no stateinfo found for the height, should have accepted the update optimistically. + seqValHash, found := s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, s.path.EndpointA.GetClientState().GetLatestHeight().GetRevisionHeight()) + s.True(found) + seqAddr, err := s.hubApp().LightClientKeeper.GetSequencerFromValHash(s.hubCtx(), s.rollappChain().ChainID, seqValHash) + s.NoError(err) + s.Equal(s.hubChain().SenderAccount.GetAddress().String(), seqAddr) +} + +func (s *lightClientSuite) TestMsgUpdateClient_StateUpdateExists_Compatible() { + s.createRollapp(false, nil) + s.registerSequencer() + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupClients(s.path) + s.NoError(s.path.EndpointA.UpdateClient()) + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID, s.path.EndpointA.ClientID) + + bds := rollapptypes.BlockDescriptors{} + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + header, err := s.path.EndpointA.Chain.ConstructUpdateTMClientHeader(s.path.EndpointA.Counterparty.Chain, s.path.EndpointA.ClientID) + s.NoError(err) + + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + msgUpdateState := rollapptypes.NewMsgUpdateState( + s.hubChain().SenderAccount.GetAddress().String(), + rollappChainID(), + "mock-da-path", + bds.BD[0].Height, uint64(len(bds.BD)), &bds, + ) + _, err = s.rollappMsgServer().UpdateState(s.hubCtx(), msgUpdateState) + s.NoError(err) + + msg, err := clienttypes.NewMsgUpdateClient( + s.path.EndpointA.ClientID, header, + s.path.EndpointA.Chain.SenderAccount.GetAddress().String(), + ) + s.NoError(err) + + // As there was compatible stateinfo found, should accept the ClientUpdate without any error. + _, err = s.path.EndpointA.Chain.SendMsgs(msg) + s.NoError(err) + s.Equal(uint64(header.Header.Height), s.path.EndpointA.GetClientState().GetLatestHeight().GetRevisionHeight()) + // There shouldnt be any optimistic updates as the roots were verified + _, found := s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, uint64(header.Header.Height)) + s.False(found) +} + +func (s *lightClientSuite) TestMsgUpdateClient_StateUpdateExists_NotCompatible() { + s.createRollapp(false, nil) + s.registerSequencer() + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupClients(s.path) + s.NoError(s.path.EndpointA.UpdateClient()) + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID, s.path.EndpointA.ClientID) + + bds := rollapptypes.BlockDescriptors{} + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + header, err := s.path.EndpointA.Chain.ConstructUpdateTMClientHeader(s.path.EndpointA.Counterparty.Chain, s.path.EndpointA.ClientID) + s.NoError(err) + + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bd.Timestamp = bd.Timestamp.AddDate(0, 0, 1) // wrong timestamp to cause state mismatch + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + msgUpdateState := rollapptypes.NewMsgUpdateState( + s.hubChain().SenderAccount.GetAddress().String(), + rollappChainID(), + "mock-da-path", + bds.BD[0].Height, uint64(len(bds.BD)), &bds, + ) + _, err = s.rollappMsgServer().UpdateState(s.hubCtx(), msgUpdateState) + s.NoError(err) + + msg, err := clienttypes.NewMsgUpdateClient( + s.path.EndpointA.ClientID, header, + s.path.EndpointA.Chain.SenderAccount.GetAddress().String(), + ) + s.NoError(err) + + // As there was incompatible stateinfo found, should prevent light client update. + s.path.EndpointA.Chain.Coordinator.UpdateTimeForChain(s.path.EndpointA.Chain) + _, _, err = simapp.SignAndDeliver( // Explicitly submitting msg as we expect it to fail + s.path.EndpointA.Chain.T, + s.path.EndpointA.Chain.TxConfig, + s.path.EndpointA.Chain.App.GetBaseApp(), + s.path.EndpointA.Chain.GetContext().BlockHeader(), + []sdk.Msg{msg}, + s.path.EndpointA.Chain.ChainID, + []uint64{s.path.EndpointA.Chain.SenderAccount.GetAccountNumber()}, + []uint64{s.path.EndpointA.Chain.SenderAccount.GetSequence()}, + true, false, s.path.EndpointA.Chain.SenderPrivKey, + ) + s.Error(err) + s.True(errorsmod.IsOf(err, types.ErrTimestampMismatch)) +} + +func (s *lightClientSuite) TestAfterUpdateState_OptimisticUpdateExists_Compatible() { + s.createRollapp(false, nil) + s.registerSequencer() + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupClients(s.path) + s.NoError(s.path.EndpointA.UpdateClient()) + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID, s.path.EndpointA.ClientID) + + bds := rollapptypes.BlockDescriptors{} + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + header, err := s.path.EndpointA.Chain.ConstructUpdateTMClientHeader(s.path.EndpointA.Counterparty.Chain, s.path.EndpointA.ClientID) + s.NoError(err) + + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + + msg, err := clienttypes.NewMsgUpdateClient( + s.path.EndpointA.ClientID, header, + s.path.EndpointA.Chain.SenderAccount.GetAddress().String(), + ) + s.NoError(err) + _, err = s.path.EndpointA.Chain.SendMsgs(msg) + s.NoError(err) + // There should be one optimistic update for the header height + _, found := s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, uint64(header.Header.Height)) + s.True(found) + + msgUpdateState := rollapptypes.NewMsgUpdateState( + s.hubChain().SenderAccount.GetAddress().String(), + rollappChainID(), + "mock-da-path", + bds.BD[0].Height, uint64(len(bds.BD)), &bds, + ) + _, err = s.rollappMsgServer().UpdateState(s.hubCtx(), msgUpdateState) + s.NoError(err) + // The optimistic update valhash should be removed as the state has been confirmed to be compatible + _, found = s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, uint64(header.Header.Height)) + s.False(found) + // Ensuring that the stateinfo is now upto date as well + state, found := s.hubApp().RollappKeeper.GetLatestStateInfo(s.hubCtx(), s.rollappChain().ChainID) + s.True(found) + s.True(state.ContainsHeight(uint64(header.Header.Height))) +} + +func (s *lightClientSuite) TestAfterUpdateState_OptimisticUpdateExists_NotCompatible() { + s.createRollapp(false, nil) + s.registerSequencer() + s.path = s.newTransferPath(s.hubChain(), s.rollappChain()) + s.coordinator.SetupConnections(s.path) + s.hubApp().LightClientKeeper.SetCanonicalClient(s.hubCtx(), s.rollappChain().ChainID, s.path.EndpointA.ClientID) + s.coordinator.CreateChannels(s.path) + s.NoError(s.path.EndpointA.UpdateClient()) + + bds := rollapptypes.BlockDescriptors{} + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + header, err := s.path.EndpointA.Chain.ConstructUpdateTMClientHeader(s.path.EndpointA.Counterparty.Chain, s.path.EndpointA.ClientID) + s.NoError(err) + + for i := 0; i < 2; i++ { + lastHeader := s.rollappChain().LastHeader + bd := rollapptypes.BlockDescriptor{Height: uint64(lastHeader.Header.Height), StateRoot: lastHeader.Header.AppHash, Timestamp: lastHeader.Header.Time} + bd.Timestamp = bd.Timestamp.AddDate(0, 0, 1) // wrong timestamp to cause state mismatch + bds.BD = append(bds.BD, bd) + s.hubChain().NextBlock() + s.rollappChain().NextBlock() + } + + msg, err := clienttypes.NewMsgUpdateClient( + s.path.EndpointA.ClientID, header, + s.path.EndpointA.Chain.SenderAccount.GetAddress().String(), + ) + s.NoError(err) + _, err = s.path.EndpointA.Chain.SendMsgs(msg) + s.NoError(err) + // There should be one optimistic update for the header height + _, found := s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, uint64(header.Header.Height)) + s.True(found) + + msgUpdateState := rollapptypes.NewMsgUpdateState( + s.hubChain().SenderAccount.GetAddress().String(), + rollappChainID(), + "mock-da-path", + bds.BD[0].Height, uint64(len(bds.BD)), &bds, + ) + _, err = s.rollappMsgServer().UpdateState(s.hubCtx(), msgUpdateState) + s.Error(err) + // The optimistic update valhash should be removed as part of fraud handling + _, found = s.hubApp().LightClientKeeper.GetConsensusStateValHash(s.hubCtx(), s.path.EndpointA.ClientID, uint64(header.Header.Height)) + s.False(found) + // Ensuring that the rollapp is now frozen as part of fraud handling + rollapp, _ := s.hubApp().RollappKeeper.GetRollapp(s.hubCtx(), s.rollappChain().ChainID) + s.True(rollapp.Frozen) +} diff --git a/ibctesting/utils_test.go b/ibctesting/utils_test.go index 15ec530b7..929bde634 100644 --- a/ibctesting/utils_test.go +++ b/ibctesting/utils_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "strings" "testing" + "time" tmrand "github.com/cometbft/cometbft/libs/rand" @@ -198,6 +199,7 @@ func (s *utilSuite) updateRollappState(endHeight uint64) { blockDescriptors.BD[i] = rollapptypes.BlockDescriptor{ Height: startHeight + uint64(i), StateRoot: bytes.Repeat([]byte{byte(startHeight) + byte(i)}, 32), + Timestamp: time.Now().UTC(), } } // Update the state diff --git a/proto/dymensionxyz/dymension/lightclient/genesis.proto b/proto/dymensionxyz/dymension/lightclient/genesis.proto new file mode 100644 index 000000000..308a1ef4c --- /dev/null +++ b/proto/dymensionxyz/dymension/lightclient/genesis.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; +package dymensionxyz.dymension.lightclient; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/dymensionxyz/dymension/v3/x/lightclient/types"; + +message GenesisState { + repeated CanonicalClient canonical_clients = 1 [ (gogoproto.nullable) = false ]; + repeated ConsensusStateSigner consensus_state_signers = 2 [ (gogoproto.nullable) = false ]; +} + +message CanonicalClient { + string rollapp_id = 1; + string ibc_client_id = 2; +} + +message ConsensusStateSigner { + // ibc_client_id is the canonical IBC client which has accepted a client update optimistically + string ibc_client_id = 1; + // height is the client height which was updated optimistically + uint64 height = 2; + // blockValHash is the valhash of the block which was updated optimistically + string blockValHash = 3; +} \ No newline at end of file diff --git a/proto/dymensionxyz/dymension/rollapp/block_descriptor.proto b/proto/dymensionxyz/dymension/rollapp/block_descriptor.proto index f3bede6cf..c7b26fb23 100644 --- a/proto/dymensionxyz/dymension/rollapp/block_descriptor.proto +++ b/proto/dymensionxyz/dymension/rollapp/block_descriptor.proto @@ -4,6 +4,7 @@ package dymensionxyz.dymension.rollapp; option go_package = "github.com/dymensionxyz/dymension/v3/x/rollapp/types"; import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; // BlockDescriptor defines a single rollapp chain block description. @@ -12,10 +13,11 @@ message BlockDescriptor { uint64 height = 1; // stateRoot is a 32 byte array of the hash of the block (state root of the block) bytes stateRoot = 2; + // timestamp is the time from the block header + google.protobuf.Timestamp timestamp = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; } // BlockDescriptors defines list of BlockDescriptor. message BlockDescriptors { - repeated BlockDescriptor BD = 1 [(gogoproto.nullable) = false]; } \ No newline at end of file diff --git a/testutil/keeper/lightclient.go b/testutil/keeper/lightclient.go new file mode 100644 index 000000000..fe68aa265 --- /dev/null +++ b/testutil/keeper/lightclient.go @@ -0,0 +1,181 @@ +package keeper + +import ( + "context" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/store" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + sequencertypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" + + cometbftdb "github.com/cometbft/cometbft-db" + "github.com/cometbft/cometbft/libs/log" + "github.com/cometbft/cometbft/libs/math" + cometbftproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/stretchr/testify/require" +) + +const ( + Alice = "dym1wg8p6j0pxpnsvhkwfu54ql62cnrumf0v634mft" +) + +func LightClientKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { + storeKey := sdk.NewKVStoreKey(types.StoreKey) + memStoreKey := storetypes.NewMemoryStoreKey(types.StoreKey + "_mem") + + db := cometbftdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) + stateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) + require.NoError(t, stateStore.LoadLatestVersion()) + + registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + sequencerPubKey := ed25519.GenPrivKey().PubKey() + tmPk, err := codectypes.NewAnyWithValue(sequencerPubKey) + require.NoError(t, err) + + testSequencer := sequencertypes.Sequencer{ + Address: Alice, + DymintPubKey: tmPk, + } + nextValHash, err := testSequencer.GetDymintPubKeyHash() + require.NoError(t, err) + testSequencers := map[string]sequencertypes.Sequencer{ + Alice: testSequencer, + } + testConsensusStates := map[string]map[uint64]exported.ConsensusState{ + "canon-client-id": { + 2: &ibctm.ConsensusState{ + Timestamp: time.Unix(1724392989, 0), + Root: commitmenttypes.NewMerkleRoot([]byte("test2")), + NextValidatorsHash: nextValHash, + }, + }, + } + cs := ibctm.NewClientState("rollapp-wants-canon-client", + ibctm.NewFractionFromTm(math.Fraction{Numerator: 1, Denominator: 1}), + time.Hour*24*7*2, time.Hour*24*7*3, time.Minute*10, + ibcclienttypes.MustParseHeight("1-2"), commitmenttypes.GetSDKSpecs(), []string{}, + ) + testGenesisClients := map[string]exported.ClientState{ + "canon-client-id": cs, + } + + mockIBCKeeper := NewMockIBCClientKeeper(testConsensusStates, testGenesisClients) + mockSequencerKeeper := NewMockSequencerKeeper(testSequencers) + mockRollappKeeper := NewMockRollappKeeper() + k := keeper.NewKeeper( + cdc, + storeKey, + mockIBCKeeper, + mockSequencerKeeper, + mockRollappKeeper, + ) + + ctx := sdk.NewContext(stateStore, cometbftproto.Header{}, false, log.NewNopLogger()) + + return k, ctx +} + +type MockIBCCLientKeeper struct { + clientConsensusState map[string]map[uint64]exported.ConsensusState + clientStates map[string]exported.ClientState +} + +func NewMockIBCClientKeeper( + clientCS map[string]map[uint64]exported.ConsensusState, + genesisClients map[string]exported.ClientState, +) *MockIBCCLientKeeper { + return &MockIBCCLientKeeper{ + clientConsensusState: clientCS, + clientStates: genesisClients, + } +} + +func (m *MockIBCCLientKeeper) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) { + cs, ok := m.clientConsensusState[clientID][height.GetRevisionHeight()] + return cs, ok +} + +func (m *MockIBCCLientKeeper) GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) { + cs, ok := m.clientStates[clientID] + return cs, ok +} + +func (m *MockIBCCLientKeeper) IterateClientStates(ctx sdk.Context, prefix []byte, cb func(clientID string, cs exported.ClientState) bool) { + for clientID, cs := range m.clientStates { + if cb(clientID, cs) { + break + } + } +} + +func (m *MockIBCCLientKeeper) ConsensusStateHeights(c context.Context, req *ibcclienttypes.QueryConsensusStateHeightsRequest) (*ibcclienttypes.QueryConsensusStateHeightsResponse, error) { + heights := []ibcclienttypes.Height{ + ibcclienttypes.NewHeight(1, 2), + } + return &ibcclienttypes.QueryConsensusStateHeightsResponse{ + ConsensusStateHeights: heights, + }, nil +} + +type MockSequencerKeeper struct { + sequencers map[string]sequencertypes.Sequencer +} + +func NewMockSequencerKeeper(sequencers map[string]sequencertypes.Sequencer) *MockSequencerKeeper { + return &MockSequencerKeeper{ + sequencers: sequencers, + } +} + +func (m *MockSequencerKeeper) GetSequencer(ctx sdk.Context, seqAddr string) (sequencertypes.Sequencer, bool) { + seq, ok := m.sequencers[seqAddr] + return seq, ok +} + +func (m *MockSequencerKeeper) GetSequencersByRollapp(ctx sdk.Context, rollappId string) (list []sequencertypes.Sequencer) { + seqs := make([]sequencertypes.Sequencer, 0, len(m.sequencers)) + for _, seq := range m.sequencers { + seqs = append(seqs, seq) + } + return seqs +} + +type MockRollappKeeper struct{} + +func NewMockRollappKeeper() *MockRollappKeeper { + return &MockRollappKeeper{} +} + +func (m *MockRollappKeeper) GetRollapp(ctx sdk.Context, rollappId string) (val rollapptypes.Rollapp, found bool) { + return rollapptypes.Rollapp{}, false +} + +func (m *MockRollappKeeper) FindStateInfoByHeight(ctx sdk.Context, rollappId string, height uint64) (*rollapptypes.StateInfo, error) { + return nil, nil +} + +func (m *MockRollappKeeper) GetStateInfo(ctx sdk.Context, rollappId string, index uint64) (val rollapptypes.StateInfo, found bool) { + return rollapptypes.StateInfo{}, false +} + +func (m *MockRollappKeeper) SetRollapp(ctx sdk.Context, rollapp rollapptypes.Rollapp) { +} + +func (m *MockRollappKeeper) HandleFraud(ctx sdk.Context, rollappID, clientId string, fraudHeight uint64, seqAddr string) error { + return nil +} diff --git a/x/dymns/keeper/hooks.go b/x/dymns/keeper/hooks.go index c94ee63e1..5047a2275 100644 --- a/x/dymns/keeper/hooks.go +++ b/x/dymns/keeper/hooks.go @@ -286,6 +286,10 @@ func (h rollappHooks) BeforeUpdateState(_ sdk.Context, _ string, _ string, _ boo return nil } +func (h rollappHooks) AfterUpdateState(_ sdk.Context, _ string, _ *rollapptypes.StateInfo) error { + return nil +} + func (h rollappHooks) AfterStateFinalized(_ sdk.Context, _ string, _ *rollapptypes.StateInfo) error { return nil } diff --git a/x/lightclient/ante/ibc_msg_channel_open_ack.go b/x/lightclient/ante/ibc_msg_channel_open_ack.go new file mode 100644 index 000000000..ebef53993 --- /dev/null +++ b/x/lightclient/ante/ibc_msg_channel_open_ack.go @@ -0,0 +1,37 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/dymensionxyz/gerr-cosmos/gerrc" +) + +func (i IBCMessagesDecorator) HandleMsgChannelOpenAck(ctx sdk.Context, msg *ibcchanneltypes.MsgChannelOpenAck) error { + if msg.PortId != ibctransfertypes.PortID { // We only care about transfer channels to mark them as canonical + return nil + } + // Check if this channel is being opened on a known canonical client + _, connection, err := i.ibcChannelKeeper.GetChannelConnection(ctx, msg.PortId, msg.ChannelId) + if err != nil { + return err + } + rollappID, found := i.lightClientKeeper.GetRollappForClientID(ctx, connection.GetClientID()) + if !found { + return nil + } + // Check if canon channel already exists for rollapp, if yes, return err + rollapp, found := i.rollappKeeper.GetRollapp(ctx, rollappID) + if !found { + return errorsmod.Wrap(gerrc.ErrInternal, "rollapp not found") + } + if rollapp.ChannelId != "" { + return errorsmod.Wrap(gerrc.ErrFailedPrecondition, "canonical channel already exists for the rollapp") + } + // Set this channel as the canonical channel for the rollapp + rollapp.ChannelId = msg.ChannelId + i.rollappKeeper.SetRollapp(ctx, rollapp) + + return nil +} diff --git a/x/lightclient/ante/ibc_msg_channel_open_ack_test.go b/x/lightclient/ante/ibc_msg_channel_open_ack_test.go new file mode 100644 index 000000000..020d88c08 --- /dev/null +++ b/x/lightclient/ante/ibc_msg_channel_open_ack_test.go @@ -0,0 +1,102 @@ +package ante_test + +import ( + "testing" + + ibcconnectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/ante" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/dymensionxyz/gerr-cosmos/gerrc" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgChannelOpenAck(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + testRollapps := map[string]rollapptypes.Rollapp{ + "rollapp-has-canon-client": { + RollappId: "rollapp-has-canon-client", + ChannelId: "channel-on-canon-client", + }, + "rollapp-no-canon-channel": { + RollappId: "rollapp-no-canon-channel", + ChannelId: "", + }, + } + testConnections := map[string]ibcconnectiontypes.ConnectionEnd{ + "new-channel-on-canon-client": { + ClientId: "canon-client-id", + }, + "first-channel-on-canon-client": { + ClientId: "canon-client-id-2", + }, + "non-canon-channel-id": { + ClientId: "non-canon-client-id", + }, + } + rollappKeeper := NewMockRollappKeeper(testRollapps, nil) + ibcclientKeeper := NewMockIBCClientKeeper(nil) + ibcchannelKeeper := NewMockIBCChannelKeeper(testConnections) + keeper.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + keeper.SetCanonicalClient(ctx, "rollapp-no-canon-channel", "canon-client-id-2") + ibcMsgDecorator := ante.NewIBCMessagesDecorator(*keeper, ibcclientKeeper, ibcchannelKeeper, rollappKeeper) + testCases := []struct { + name string + inputMsg ibcchanneltypes.MsgChannelOpenAck + err error + canonChannelSet bool + }{ + { + name: "port id is not transfer port", + inputMsg: ibcchanneltypes.MsgChannelOpenAck{ + PortId: "not-transfer-port", + ChannelId: "channel-id", + }, + err: nil, + canonChannelSet: false, + }, + { + name: "channel not on a canonical client", + inputMsg: ibcchanneltypes.MsgChannelOpenAck{ + PortId: "transfer", + ChannelId: "non-canon-channel-id", + }, + err: nil, + canonChannelSet: false, + }, + { + name: "canonical channel already exists for rollapp", + inputMsg: ibcchanneltypes.MsgChannelOpenAck{ + PortId: "transfer", + ChannelId: "new-channel-on-canon-client", + }, + err: gerrc.ErrFailedPrecondition, + canonChannelSet: false, + }, + { + name: "canonical channel does not exist - set new channel as canonical", + inputMsg: ibcchanneltypes.MsgChannelOpenAck{ + PortId: "transfer", + ChannelId: "first-channel-on-canon-client", + }, + err: nil, + canonChannelSet: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ibcMsgDecorator.HandleMsgChannelOpenAck(ctx, &tc.inputMsg) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + } + if tc.canonChannelSet { + rollapp, found := rollappKeeper.GetRollapp(ctx, "rollapp-no-canon-channel") + require.True(t, found) + require.Equal(t, tc.inputMsg.ChannelId, rollapp.ChannelId) + } + }) + } +} diff --git a/x/lightclient/ante/ibc_msg_submit_misbehaviour.go b/x/lightclient/ante/ibc_msg_submit_misbehaviour.go new file mode 100644 index 000000000..e0a991ff8 --- /dev/null +++ b/x/lightclient/ante/ibc_msg_submit_misbehaviour.go @@ -0,0 +1,27 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" +) + +func (i IBCMessagesDecorator) HandleMsgSubmitMisbehaviour(ctx sdk.Context, msg *ibcclienttypes.MsgSubmitMisbehaviour) error { + clientState, found := i.ibcClientKeeper.GetClientState(ctx, msg.ClientId) + if !found { + return nil + } + // Cast client state to tendermint client state - we need this to get the chain id + tendmermintClientState, ok := clientState.(*ibctm.ClientState) + if !ok { + return nil + } + // Check if the client is the canonical client for a rollapp + rollappID := tendmermintClientState.ChainId + canonicalClient, _ := i.lightClientKeeper.GetCanonicalClient(ctx, rollappID) + if canonicalClient == msg.ClientId { + return errorsmod.Wrap(ibcclienttypes.ErrInvalidClient, "cannot submit misbehavour for a canonical client") + } + return nil +} diff --git a/x/lightclient/ante/ibc_msg_submit_misbehaviour_test.go b/x/lightclient/ante/ibc_msg_submit_misbehaviour_test.go new file mode 100644 index 000000000..5ea75fc23 --- /dev/null +++ b/x/lightclient/ante/ibc_msg_submit_misbehaviour_test.go @@ -0,0 +1,69 @@ +package ante_test + +import ( + "testing" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibcsolomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + + keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/ante" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgSubmitMisbehaviour(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + rollappKeeper := NewMockRollappKeeper(nil, nil) + testClientStates := map[string]exported.ClientState{ + "non-tm-client-id": &ibcsolomachine.ClientState{}, + "canon-client-id": &ibctm.ClientState{ + ChainId: "rollapp-has-canon-client", + }, + } + ibcclientKeeper := NewMockIBCClientKeeper(testClientStates) + ibcchannelKeeper := NewMockIBCChannelKeeper(nil) + keeper.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + ibcMsgDecorator := ante.NewIBCMessagesDecorator(*keeper, ibcclientKeeper, ibcchannelKeeper, rollappKeeper) + testCases := []struct { + name string + inputMsg ibcclienttypes.MsgSubmitMisbehaviour + err error + }{ + { + name: "Could not unpack light client state as tendermint client state", + inputMsg: ibcclienttypes.MsgSubmitMisbehaviour{ + ClientId: "non-tm-client-id", + Misbehaviour: nil, + }, + err: nil, + }, + { + name: "Client is a known canonical client for a rollapp", + inputMsg: ibcclienttypes.MsgSubmitMisbehaviour{ + ClientId: "canon-client-id", + Misbehaviour: nil, + }, + err: ibcclienttypes.ErrInvalidClient, + }, + { + name: "Client is not a known canonical client", + inputMsg: ibcclienttypes.MsgSubmitMisbehaviour{ + ClientId: "client-id", + Misbehaviour: nil, + }, + err: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ibcMsgDecorator.HandleMsgSubmitMisbehaviour(ctx, &tc.inputMsg) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/lightclient/ante/ibc_msg_update_client.go b/x/lightclient/ante/ibc_msg_update_client.go new file mode 100644 index 000000000..5caf22e73 --- /dev/null +++ b/x/lightclient/ante/ibc_msg_update_client.go @@ -0,0 +1,75 @@ +package ante + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "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/x/lightclient/types" +) + +func (i IBCMessagesDecorator) HandleMsgUpdateClient(ctx sdk.Context, msg *ibcclienttypes.MsgUpdateClient) error { + clientState, found := i.ibcClientKeeper.GetClientState(ctx, msg.ClientId) + if !found { + return nil + } + // Cast client state to tendermint client state - we need this to get the chain id(rollapp id) + tmClientState, ok := clientState.(*ibctm.ClientState) + if !ok { + return nil + } + // Check if the client is the canonical client for the rollapp + rollappID := tmClientState.ChainId + canonicalClient, _ := i.lightClientKeeper.GetCanonicalClient(ctx, rollappID) + if canonicalClient != msg.ClientId { + return nil // The client is not a rollapp's canonical client. Continue with default behaviour. + } + clientMessage, err := ibcclienttypes.UnpackClientMessage(msg.ClientMessage) + if err != nil { + return nil + } + header, ok := clientMessage.(*ibctm.Header) + if !ok { + return nil + } + + // Check if there are existing block descriptors for the given height of client state + height := uint64(header.Header.Height) + stateInfo, err := i.rollappKeeper.FindStateInfoByHeight(ctx, rollappID, height) + if err != nil { + // No BDs found for given height. + // Will accept the update optimistically + // But also save the blockProposer address with the height for future verification + i.acceptUpdateOptimistically(ctx, msg.ClientId, header) + return nil + } + bd, _ := stateInfo.GetBlockDescriptor(height) + + stateInfo, err = i.rollappKeeper.FindStateInfoByHeight(ctx, rollappID, height+1) + if err != nil { + // No BDs found for next height. + // Will accept the update optimistically + // But also save the blockProposer address with the height for future verification + i.acceptUpdateOptimistically(ctx, msg.ClientId, header) + return nil + } + sequencerPubKey, err := i.lightClientKeeper.GetSequencerPubKey(ctx, stateInfo.Sequencer) + if err != nil { + return err + } + rollappState := types.RollappState{ + BlockDescriptor: bd, + NextBlockSequencer: sequencerPubKey, + } + // Ensure that the ibc header is compatible with the existing rollapp state + // If it's not, we error and prevent the MsgUpdateClient from being processed + err = types.CheckCompatibility(*header.ConsensusState(), rollappState) + if err != nil { + return err + } + + return nil +} + +func (i IBCMessagesDecorator) acceptUpdateOptimistically(ctx sdk.Context, clientID string, header *ibctm.Header) { + i.lightClientKeeper.SetConsensusStateValHash(ctx, clientID, uint64(header.Header.Height), header.Header.ValidatorsHash) +} diff --git a/x/lightclient/ante/ibc_msg_update_client_test.go b/x/lightclient/ante/ibc_msg_update_client_test.go new file mode 100644 index 000000000..c17e3e67c --- /dev/null +++ b/x/lightclient/ante/ibc_msg_update_client_test.go @@ -0,0 +1,315 @@ +package ante_test + +import ( + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibcsolomachine "github.com/cosmos/ibc-go/v7/modules/light-clients/06-solomachine" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/ante" + "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgUpdateClient(t *testing.T) { + type testInput struct { + msg *ibcclienttypes.MsgUpdateClient + rollapps map[string]rollapptypes.Rollapp + stateInfos map[string]map[uint64]rollapptypes.StateInfo + } + testCases := []struct { + name string + prepare func(ctx sdk.Context, k keeper.Keeper) testInput + assert func(ctx sdk.Context, k keeper.Keeper, err error) + }{ + { + name: "Could not find a client with given client id", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "non-existent-client", + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.NoError(t, err) + }, + }, + { + name: "Could not unpack as tendermint client state", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "non-tm-client-id", + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.NoError(t, err) + }, + }, + { + name: "Client is not a known canonical client of a rollapp", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "canon-client-id", + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.NoError(t, err) + }, + }, + { + name: "Could not find state info for height - ensure optimistically accepted and signer stored in state", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + k.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + seqValHash, err := k.GetSequencerHash(ctx, keepertest.Alice) + require.NoError(t, err) + var valSet, trustedVals *cmtproto.ValidatorSet + signedHeader := &cmtproto.SignedHeader{ + Header: &cmtproto.Header{ + ValidatorsHash: seqValHash, + Height: 1, + }, + Commit: &cmtproto.Commit{}, + } + header := ibctm.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: ibcclienttypes.MustParseHeight("1-1"), + TrustedValidators: trustedVals, + } + clientMsg, err := ibcclienttypes.PackClientMessage(&header) + require.NoError(t, err) + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "canon-client-id", + ClientMessage: clientMsg, + Signer: "relayerAddr", + }, + rollapps: map[string]rollapptypes.Rollapp{ + "rollapp-has-canon-client": { + RollappId: "rollapp-has-canon-client", + }, + }, + stateInfos: map[string]map[uint64]rollapptypes.StateInfo{ + "rollapp-has-canon-client": { + 3: { + Sequencer: keepertest.Alice, + StateInfoIndex: rollapptypes.StateInfoIndex{ + Index: 3, + }, + StartHeight: 3, + NumBlocks: 1, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 3, + StateRoot: []byte{}, + Timestamp: time.Unix(1724392989, 0), + }, + }, + }, + }, + }, + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.NoError(t, err) + seqValHash, found := k.GetConsensusStateValHash(ctx, "canon-client-id", 1) + require.True(t, found) + seq, err := k.GetSequencerFromValHash(ctx, "rollapp-has-canon-client", seqValHash) + require.NoError(t, err) + require.Equal(t, keepertest.Alice, seq) + }, + }, + { + name: "State is incompatible - do not accept", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + k.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + var ( + valSet *cmtproto.ValidatorSet + trustedVals *cmtproto.ValidatorSet + ) + signedHeader := &cmtproto.SignedHeader{ + Header: &cmtproto.Header{ + AppHash: []byte("appHash"), + ProposerAddress: []byte("sequencerAddr"), + Time: time.Unix(1724392989, 0), + NextValidatorsHash: []byte("nextValHash"), + Height: 1, + }, + Commit: &cmtproto.Commit{}, + } + header := ibctm.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: ibcclienttypes.MustParseHeight("1-1"), + TrustedValidators: trustedVals, + } + clientMsg, err := ibcclienttypes.PackClientMessage(&header) + require.NoError(t, err) + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "canon-client-id", + ClientMessage: clientMsg, + Signer: "sequencerAddr", + }, + rollapps: map[string]rollapptypes.Rollapp{ + "rollapp-has-canon-client": { + RollappId: "rollapp-has-canon-client", + }, + }, + stateInfos: map[string]map[uint64]rollapptypes.StateInfo{ + "rollapp-has-canon-client": { + 1: { + Sequencer: keepertest.Alice, + StateInfoIndex: rollapptypes.StateInfoIndex{ + Index: 1, + }, + StartHeight: 1, + NumBlocks: 1, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 1, + StateRoot: []byte{}, + Timestamp: time.Unix(1724392989, 0), + }, + }, + }, + }, + 2: { + Sequencer: keepertest.Alice, + StateInfoIndex: rollapptypes.StateInfoIndex{ + Index: 2, + }, + StartHeight: 2, + NumBlocks: 1, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 2, + StateRoot: []byte("appHash2"), + Timestamp: time.Unix(1724392989, 0), + }, + }, + }, + }, + }, + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.ErrorIs(t, err, types.ErrStateRootsMismatch) + }, + }, + { + name: "Ensure state is compatible - happy path", + prepare: func(ctx sdk.Context, k keeper.Keeper) testInput { + sequencer := keepertest.Alice + proposerAddr, err := k.GetSequencerPubKey(ctx, sequencer) + require.NoError(t, err) + proposerAddrBytes, err := proposerAddr.Marshal() + require.NoError(t, err) + blocktimestamp := time.Unix(1724392989, 0) + k.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + var ( + valSet *cmtproto.ValidatorSet + trustedVals *cmtproto.ValidatorSet + ) + nextValsHash, err := k.GetSequencerHash(ctx, sequencer) + require.NoError(t, err) + signedHeader := &cmtproto.SignedHeader{ + Header: &cmtproto.Header{ + AppHash: []byte("appHash"), + ProposerAddress: proposerAddrBytes, + Time: blocktimestamp, + ValidatorsHash: nextValsHash, + NextValidatorsHash: nextValsHash, + Height: 1, + }, + Commit: &cmtproto.Commit{}, + } + header := ibctm.Header{ + SignedHeader: signedHeader, + ValidatorSet: valSet, + TrustedHeight: ibcclienttypes.MustParseHeight("1-1"), + TrustedValidators: trustedVals, + } + clientMsg, err := ibcclienttypes.PackClientMessage(&header) + require.NoError(t, err) + return testInput{ + msg: &ibcclienttypes.MsgUpdateClient{ + ClientId: "canon-client-id", + ClientMessage: clientMsg, + Signer: "relayerAddr", + }, + rollapps: map[string]rollapptypes.Rollapp{ + "rollapp-has-canon-client": { + RollappId: "rollapp-has-canon-client", + }, + }, + stateInfos: map[string]map[uint64]rollapptypes.StateInfo{ + "rollapp-has-canon-client": { + 1: { + Sequencer: keepertest.Alice, + StateInfoIndex: rollapptypes.StateInfoIndex{ + Index: 1, + }, + StartHeight: 1, + NumBlocks: 2, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 1, + StateRoot: []byte("appHash"), + Timestamp: blocktimestamp, + }, + { + Height: 2, + StateRoot: []byte("appHash2"), + Timestamp: blocktimestamp.Add(1), + }, + }, + }, + }, + }, + }, + } + }, + assert: func(ctx sdk.Context, k keeper.Keeper, err error) { + require.NoError(t, err) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + testClientStates := map[string]exported.ClientState{ + "non-tm-client-id": &ibcsolomachine.ClientState{}, + "canon-client-id": &ibctm.ClientState{ + ChainId: "rollapp-has-canon-client", + }, + } + ibcclientKeeper := NewMockIBCClientKeeper(testClientStates) + ibcchannelKeeper := NewMockIBCChannelKeeper(nil) + input := tc.prepare(ctx, *keeper) + rollappKeeper := NewMockRollappKeeper(input.rollapps, input.stateInfos) + ibcMsgDecorator := ante.NewIBCMessagesDecorator(*keeper, ibcclientKeeper, ibcchannelKeeper, rollappKeeper) + + err := ibcMsgDecorator.HandleMsgUpdateClient(ctx, input.msg) + tc.assert(ctx, *keeper, err) + }) + } +} diff --git a/x/lightclient/ante/ibc_msgs.go b/x/lightclient/ante/ibc_msgs.go new file mode 100644 index 000000000..5148fd8db --- /dev/null +++ b/x/lightclient/ante/ibc_msgs.go @@ -0,0 +1,51 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +var _ sdk.AnteDecorator = IBCMessagesDecorator{} + +type IBCMessagesDecorator struct { + ibcClientKeeper types.IBCClientKeeperExpected + ibcChannelKeeper types.IBCChannelKeeperExpected + rollappKeeper types.RollappKeeperExpected + lightClientKeeper keeper.Keeper +} + +func NewIBCMessagesDecorator(k keeper.Keeper, ibcClient types.IBCClientKeeperExpected, ibcChannel types.IBCChannelKeeperExpected, rk types.RollappKeeperExpected) IBCMessagesDecorator { + return IBCMessagesDecorator{ + ibcClientKeeper: ibcClient, + ibcChannelKeeper: ibcChannel, + rollappKeeper: rk, + lightClientKeeper: k, + } +} + +func (i IBCMessagesDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + msgs := tx.GetMsgs() + for _, m := range msgs { + switch msg := m.(type) { + case *ibcclienttypes.MsgSubmitMisbehaviour: + if err := i.HandleMsgSubmitMisbehaviour(ctx, msg); err != nil { + return ctx, errorsmod.Wrap(err, "failed to handle MsgSubmitMisbehaviour") + } + case *ibcclienttypes.MsgUpdateClient: + if err := i.HandleMsgUpdateClient(ctx, msg); err != nil { + return ctx, errorsmod.Wrap(err, "failed to handle MsgUpdateClient") + } + case *ibcchanneltypes.MsgChannelOpenAck: + if err := i.HandleMsgChannelOpenAck(ctx, msg); err != nil { + return ctx, errorsmod.Wrap(err, "failed to handle MsgChannelOpenAck") + } + default: + continue + } + } + return next(ctx, tx, simulate) +} diff --git a/x/lightclient/ante/ibc_msgs_test.go b/x/lightclient/ante/ibc_msgs_test.go new file mode 100644 index 000000000..eb8ae92e7 --- /dev/null +++ b/x/lightclient/ante/ibc_msgs_test.go @@ -0,0 +1,101 @@ +package ante_test + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" +) + +type MockRollappKeeper struct { + rollapps map[string]rollapptypes.Rollapp + stateInfos map[string]map[uint64]rollapptypes.StateInfo +} + +func NewMockRollappKeeper(rollapps map[string]rollapptypes.Rollapp, stateInfos map[string]map[uint64]rollapptypes.StateInfo) *MockRollappKeeper { + return &MockRollappKeeper{ + rollapps: rollapps, + stateInfos: stateInfos, + } +} + +func (m *MockRollappKeeper) GetRollapp(ctx sdk.Context, rollappId string) (val rollapptypes.Rollapp, found bool) { + val, found = m.rollapps[rollappId] + return val, found +} + +func (m *MockRollappKeeper) SetRollapp(ctx sdk.Context, rollapp rollapptypes.Rollapp) { + m.rollapps[rollapp.RollappId] = rollapp +} + +func (m *MockRollappKeeper) FindStateInfoByHeight(ctx sdk.Context, rollappId string, height uint64) (*rollapptypes.StateInfo, error) { + stateInfos, found := m.stateInfos[rollappId] + if !found { + return nil, rollapptypes.ErrUnknownRollappID + } + stateInfo, found := stateInfos[height] + if !found { + return nil, rollapptypes.ErrNotFound + } + return &stateInfo, nil +} + +func (m *MockRollappKeeper) GetStateInfo(ctx sdk.Context, rollappId string, index uint64) (val rollapptypes.StateInfo, found bool) { + stateInfos, found := m.stateInfos[rollappId] + if !found { + return val, false + } + val, found = stateInfos[index] + return val, found +} + +func (m *MockRollappKeeper) HandleFraud(ctx sdk.Context, rollappID, clientId string, fraudHeight uint64, seqAddr string) error { + return nil +} + +type MockIBCClientKeeper struct { + clientStates map[string]exported.ClientState +} + +func NewMockIBCClientKeeper(cs map[string]exported.ClientState) *MockIBCClientKeeper { + return &MockIBCClientKeeper{ + clientStates: cs, + } +} + +func (m *MockIBCClientKeeper) GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) { + return nil, false +} + +func (m *MockIBCClientKeeper) GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) { + val, found := m.clientStates[clientID] + return val, found +} + +func (m *MockIBCClientKeeper) IterateClientStates(ctx sdk.Context, prefix []byte, cb func(clientID string, cs exported.ClientState) bool) { +} + +func (m *MockIBCClientKeeper) ConsensusStateHeights(c context.Context, req *ibcclienttypes.QueryConsensusStateHeightsRequest) (*ibcclienttypes.QueryConsensusStateHeightsResponse, error) { + return nil, nil +} + +type MockIBCChannelKeeper struct { + channelConnections map[string]ibcconnectiontypes.ConnectionEnd +} + +func NewMockIBCChannelKeeper(connections map[string]ibcconnectiontypes.ConnectionEnd) *MockIBCChannelKeeper { + return &MockIBCChannelKeeper{ + channelConnections: connections, + } +} + +func (m *MockIBCChannelKeeper) GetChannelConnection(ctx sdk.Context, portID, channelID string) (string, exported.ConnectionI, error) { + if portID == "transfer" { + return "", m.channelConnections[channelID], nil + } + return "", nil, nil +} diff --git a/x/lightclient/keeper/canonical_client.go b/x/lightclient/keeper/canonical_client.go new file mode 100644 index 000000000..e6cc86204 --- /dev/null +++ b/x/lightclient/keeper/canonical_client.go @@ -0,0 +1,106 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +// GetProspectiveCanonicalClient returns the client id of the first IBC client which can be set as the canonical client for the given rollapp. +// The canonical client criteria are: +// 1. The client must be a tendermint client. +// 2. The client state must match the expected client params as configured by the module +// 3. All the existing consensus states much match the corresponding height rollapp block descriptors +func (k Keeper) GetProspectiveCanonicalClient(ctx sdk.Context, rollappId string, maxHeight uint64) (clientID string, stateCompatible bool) { + k.ibcClientKeeper.IterateClientStates(ctx, nil, func(client string, cs exported.ClientState) bool { + ok := k.isValidClient(ctx, client, cs, rollappId, maxHeight) + if ok { + clientID = client + stateCompatible = true + return true + } + return false + }) + return +} + +func (k Keeper) GetCanonicalClient(ctx sdk.Context, rollappId string) (string, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.GetRollappClientKey(rollappId)) + if bz == nil { + return "", false + } + return string(bz), true +} + +func (k Keeper) SetCanonicalClient(ctx sdk.Context, rollappId string, clientID string) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GetRollappClientKey(rollappId), []byte(clientID)) + store.Set(types.CanonicalClientKey(clientID), []byte(rollappId)) +} + +func (k Keeper) GetAllCanonicalClients(ctx sdk.Context) (clients []types.CanonicalClient) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.RollappClientKey) + defer iterator.Close() // nolint: errcheck + for ; iterator.Valid(); iterator.Next() { + clients = append(clients, types.CanonicalClient{ + RollappId: string(iterator.Key()[1:]), + IbcClientId: string(iterator.Value()), + }) + } + return +} + +func (k Keeper) isValidClient(ctx sdk.Context, clientID string, cs exported.ClientState, rollappId string, maxHeight uint64) bool { + tmClientState, ok := cs.(*ibctm.ClientState) + if !ok { + return false + } + if tmClientState.ChainId != rollappId { + return false + } + if !types.IsCanonicalClientParamsValid(tmClientState) { + return false + } + res, err := k.ibcClientKeeper.ConsensusStateHeights(ctx, &ibcclienttypes.QueryConsensusStateHeightsRequest{ + ClientId: clientID, + Pagination: &query.PageRequest{Limit: maxHeight}, + }) + if err != nil { + return false + } + for _, consensusHeight := range res.ConsensusStateHeights { + h := consensusHeight.GetRevisionHeight() + if maxHeight < h { + break + } + consensusState, _ := k.ibcClientKeeper.GetClientConsensusState(ctx, clientID, consensusHeight) + tmConsensusState, _ := consensusState.(*ibctm.ConsensusState) + stateInfoH, err := k.rollappKeeper.FindStateInfoByHeight(ctx, rollappId, h) + if err != nil { + return false + } + stateInfoHplus1, err := k.rollappKeeper.FindStateInfoByHeight(ctx, rollappId, h+1) + if err != nil { + return false + } + bd, _ := stateInfoH.GetBlockDescriptor(h) + oldSequencer, err := k.GetSequencerPubKey(ctx, stateInfoHplus1.Sequencer) + if err != nil { + return false + } + rollappState := types.RollappState{ + BlockDescriptor: bd, + NextBlockSequencer: oldSequencer, + } + err = types.CheckCompatibility(*tmConsensusState, rollappState) + if err != nil { + return false + } + } + return true +} diff --git a/x/lightclient/keeper/genesis.go b/x/lightclient/keeper/genesis.go new file mode 100644 index 000000000..19b4f5260 --- /dev/null +++ b/x/lightclient/keeper/genesis.go @@ -0,0 +1,27 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +func (k Keeper) InitGenesis(ctx sdk.Context, genesisState types.GenesisState) { + if err := genesisState.Validate(); err != nil { + panic(err) + } + for _, client := range genesisState.GetCanonicalClients() { + k.SetCanonicalClient(ctx, client.RollappId, client.IbcClientId) + } + for _, stateSigner := range genesisState.GetConsensusStateSigners() { + k.SetConsensusStateValHash(ctx, stateSigner.IbcClientId, stateSigner.Height, []byte(stateSigner.BlockValHash)) + } +} + +func (k Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { + clients := k.GetAllCanonicalClients(ctx) + stateSigners := k.GetAllConsensusStateSigners(ctx) + return types.GenesisState{ + CanonicalClients: clients, + ConsensusStateSigners: stateSigners, + } +} diff --git a/x/lightclient/keeper/genesis_test.go b/x/lightclient/keeper/genesis_test.go new file mode 100644 index 000000000..657c96814 --- /dev/null +++ b/x/lightclient/keeper/genesis_test.go @@ -0,0 +1,64 @@ +package keeper_test + +import ( + "testing" + + keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + "github.com/stretchr/testify/require" +) + +func TestInitGenesis(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + clients := []types.CanonicalClient{ + {RollappId: "rollapp-1", IbcClientId: "client-1"}, + {RollappId: "rollapp-2", IbcClientId: "client-2"}, + } + stateSigners := []types.ConsensusStateSigner{ + {IbcClientId: "client-1", Height: 1, BlockValHash: "signer-1"}, + {IbcClientId: "client-1", Height: 2, BlockValHash: "signer-1"}, + } + + keeper.InitGenesis(ctx, types.GenesisState{ + CanonicalClients: clients, + ConsensusStateSigners: stateSigners, + }) + + ibc, found := keeper.GetCanonicalClient(ctx, "rollapp-1") + require.True(t, found) + require.Equal(t, "client-1", ibc) + ibc, found = keeper.GetCanonicalClient(ctx, "rollapp-2") + require.True(t, found) + require.Equal(t, "client-2", ibc) + + signer, found := keeper.GetConsensusStateValHash(ctx, "client-1", 1) + require.True(t, found) + require.Equal(t, []byte("signer-1"), signer) + signer, found = keeper.GetConsensusStateValHash(ctx, "client-1", 2) + require.True(t, found) + require.Equal(t, []byte("signer-1"), signer) +} + +func TestExportGenesis(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + + keeper.SetCanonicalClient(ctx, "rollapp-1", "client-1") + keeper.SetCanonicalClient(ctx, "rollapp-2", "client-2") + keeper.SetConsensusStateValHash(ctx, "client-1", 1, []byte("signer-1")) + keeper.SetConsensusStateValHash(ctx, "client-1", 2, []byte("signer-1")) + + genesis := keeper.ExportGenesis(ctx) + + require.Len(t, genesis.CanonicalClients, 2) + require.Equal(t, "client-1", genesis.CanonicalClients[0].IbcClientId) + require.Equal(t, "client-2", genesis.CanonicalClients[1].IbcClientId) + require.Equal(t, "rollapp-1", genesis.CanonicalClients[0].RollappId) + require.Equal(t, "rollapp-2", genesis.CanonicalClients[1].RollappId) + require.Len(t, genesis.ConsensusStateSigners, 2) + require.Equal(t, "client-1", genesis.ConsensusStateSigners[0].IbcClientId) + require.Equal(t, "client-1", genesis.ConsensusStateSigners[1].IbcClientId) + require.Equal(t, uint64(1), genesis.ConsensusStateSigners[0].Height) + require.Equal(t, uint64(2), genesis.ConsensusStateSigners[1].Height) + require.Equal(t, "signer-1", genesis.ConsensusStateSigners[0].BlockValHash) + require.Equal(t, "signer-1", genesis.ConsensusStateSigners[1].BlockValHash) +} diff --git a/x/lightclient/keeper/hook_listener.go b/x/lightclient/keeper/hook_listener.go new file mode 100644 index 000000000..315419831 --- /dev/null +++ b/x/lightclient/keeper/hook_listener.go @@ -0,0 +1,105 @@ +package keeper + +import ( + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "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/x/lightclient/types" + + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" +) + +var _ rollapptypes.RollappHooks = rollappHook{} + +// Hooks wrapper struct for rollapp keeper. +type rollappHook struct { + rollapptypes.StubRollappCreatedHooks + k Keeper +} + +// RollappHooks returns the wrapper struct. +func (k Keeper) RollappHooks() rollapptypes.RollappHooks { + return rollappHook{k: k} +} + +// AfterUpdateState is called after a state update is made to a rollapp. +// This hook checks if the rollapp has a canonical IBC light client and if the Consensus state is compatible with the state update +// and punishes the sequencer if it is not +func (hook rollappHook) AfterUpdateState( + ctx sdk.Context, + rollappId string, + stateInfo *rollapptypes.StateInfo, +) error { + canonicalClient, found := hook.k.GetCanonicalClient(ctx, rollappId) + if !found { + canonicalClient, foundClient := hook.k.GetProspectiveCanonicalClient(ctx, rollappId, stateInfo.GetLatestHeight()-1) + if foundClient { + hook.k.SetCanonicalClient(ctx, rollappId, canonicalClient) + } + return nil + } + sequencerPk, err := hook.k.GetSequencerPubKey(ctx, stateInfo.Sequencer) + if err != nil { + return err + } + latestHeight := stateInfo.GetLatestHeight() + // We check from latestHeight-1 downwards, as the nextValHash for latestHeight will not be available until next stateupdate + for h := latestHeight - 1; h >= stateInfo.StartHeight; h-- { + bd, _ := stateInfo.GetBlockDescriptor(h) + // Check if any optimistic updates were made for the given height + blockValHash, found := hook.k.GetConsensusStateValHash(ctx, canonicalClient, bd.GetHeight()) + if !found { + continue + } + err := hook.checkStateForHeight(ctx, rollappId, bd, canonicalClient, sequencerPk, blockValHash) + if err != nil { + return err + } + } + // Check for the last BD from the previous stateInfo as now we have the nextValhash available for that block + blockValHash, found := hook.k.GetConsensusStateValHash(ctx, canonicalClient, stateInfo.StartHeight-1) + if found { + previousStateInfo, err := hook.k.rollappKeeper.FindStateInfoByHeight(ctx, rollappId, stateInfo.StartHeight-1) + if err != nil { + return err + } + bd, _ := previousStateInfo.GetBlockDescriptor(stateInfo.StartHeight - 1) + err = hook.checkStateForHeight(ctx, rollappId, bd, canonicalClient, sequencerPk, blockValHash) + if err != nil { + return err + } + } + return nil +} + +func (hook rollappHook) checkStateForHeight(ctx sdk.Context, rollappId string, bd rollapptypes.BlockDescriptor, canonicalClient string, sequencerPk tmprotocrypto.PublicKey, blockValHash []byte) error { + cs, _ := hook.k.ibcClientKeeper.GetClientState(ctx, canonicalClient) + height := ibcclienttypes.NewHeight(cs.GetLatestHeight().GetRevisionNumber(), bd.GetHeight()) + consensusState, _ := hook.k.ibcClientKeeper.GetClientConsensusState(ctx, canonicalClient, height) + // Cast consensus state to tendermint consensus state - we need this to check the state root and timestamp and nextValHash + tmConsensusState, ok := consensusState.(*ibctm.ConsensusState) + if !ok { + return nil + } + rollappState := types.RollappState{ + BlockDescriptor: bd, + NextBlockSequencer: sequencerPk, + } + err := types.CheckCompatibility(*tmConsensusState, rollappState) + if err != nil { + // If the state is not compatible, + // Take this state update as source of truth over the IBC update + // Punish the block proposer of the IBC signed header + sequencerAddress, err := hook.k.GetSequencerFromValHash(ctx, rollappId, blockValHash) + if err != nil { + return err + } + err = hook.k.rollappKeeper.HandleFraud(ctx, rollappId, canonicalClient, bd.GetHeight(), sequencerAddress) + if err != nil { + return err + } + } + hook.k.RemoveConsensusStateValHash(ctx, canonicalClient, bd.GetHeight()) + return nil +} diff --git a/x/lightclient/keeper/hook_listener_test.go b/x/lightclient/keeper/hook_listener_test.go new file mode 100644 index 000000000..b6386d338 --- /dev/null +++ b/x/lightclient/keeper/hook_listener_test.go @@ -0,0 +1,146 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" + lightClientKeeper "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/stretchr/testify/require" +) + +type testInput struct { + rollappId string + stateInfo *rollapptypes.StateInfo +} + +func TestAfterUpdateState(t *testing.T) { + testCases := []struct { + name string + prepare func(ctx sdk.Context, k lightClientKeeper.Keeper) testInput + expectErr bool + }{ + { + name: "canonical client does not exist for rollapp", + prepare: func(ctx sdk.Context, k lightClientKeeper.Keeper) testInput { + return testInput{ + rollappId: "rollapp-no-canon-client", + stateInfo: &rollapptypes.StateInfo{}, + } + }, + expectErr: false, + }, + { + name: "canonical client exists but consensus state is not found for given height", + prepare: func(ctx sdk.Context, k lightClientKeeper.Keeper) testInput { + k.SetCanonicalClient(ctx, "rollapp-has-canon-client-but-no-state", "canon-client-id-no-state") + return testInput{ + rollappId: "rollapp-has-canon-client-but-no-state", + stateInfo: &rollapptypes.StateInfo{ + Sequencer: keepertest.Alice, + StartHeight: 1, + NumBlocks: 1, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 1, + StateRoot: []byte("test"), + Timestamp: time.Unix(1724392989, 0), + }, + }, + }, + }, + } + }, + expectErr: false, + }, + { + name: "both states are not compatible - slash the sequencer who signed", + prepare: func(ctx sdk.Context, k lightClientKeeper.Keeper) testInput { + k.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + seqValHash, err := k.GetSequencerHash(ctx, keepertest.Alice) + require.NoError(t, err) + k.SetConsensusStateValHash(ctx, "canon-client-id", 2, seqValHash) + return testInput{ + rollappId: "rollapp-has-canon-client", + stateInfo: &rollapptypes.StateInfo{ + Sequencer: keepertest.Alice, + StartHeight: 1, + NumBlocks: 3, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 1, + StateRoot: []byte("test"), + Timestamp: time.Unix(1724392989, 0), + }, + { + Height: 2, + StateRoot: []byte("this is not compatible"), + Timestamp: time.Unix(1724392989, 0).Add(1), + }, + { + Height: 3, + StateRoot: []byte("test3"), + Timestamp: time.Unix(1724392989, 0).Add(2), + }, + }, + }, + }, + } + }, + expectErr: false, + }, + { + name: "state is compatible", + prepare: func(ctx sdk.Context, k lightClientKeeper.Keeper) testInput { + k.SetCanonicalClient(ctx, "rollapp-has-canon-client", "canon-client-id") + seqValHash, err := k.GetSequencerHash(ctx, keepertest.Alice) + require.NoError(t, err) + k.SetConsensusStateValHash(ctx, "canon-client-id", 2, seqValHash) + return testInput{ + rollappId: "rollapp-has-canon-client", + stateInfo: &rollapptypes.StateInfo{ + Sequencer: keepertest.Alice, + StartHeight: 1, + NumBlocks: 3, + BDs: rollapptypes.BlockDescriptors{ + BD: []rollapptypes.BlockDescriptor{ + { + Height: 1, + StateRoot: []byte("test"), + Timestamp: time.Unix(1724392989, 0), + }, + { + Height: 2, + StateRoot: []byte("test2"), + Timestamp: time.Unix(1724392989, 0), + }, + { + Height: 3, + StateRoot: []byte("test3"), + Timestamp: time.Unix(1724392989, 0).Add(1), + }, + }, + }, + }, + } + }, + expectErr: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + keeper, ctx := keepertest.LightClientKeeper(t) + input := tc.prepare(ctx, *keeper) + err := keeper.RollappHooks().AfterUpdateState(ctx, input.rollappId, input.stateInfo) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/lightclient/keeper/invariants.go b/x/lightclient/keeper/invariants.go new file mode 100644 index 000000000..94804135e --- /dev/null +++ b/x/lightclient/keeper/invariants.go @@ -0,0 +1,50 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +// RegisterInvariants registers the lightclient module invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + ir.RegisterRoute(types.ModuleName, "canonical-client-valid", CanonicalClientsValid(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 + ) + clients := k.GetAllCanonicalClients(ctx) + 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" + } + } + + return sdk.FormatInvariant( + types.ModuleName, "canonical-client-valid", + msg, + ), broken + } +} diff --git a/x/lightclient/keeper/keeper.go b/x/lightclient/keeper/keeper.go new file mode 100644 index 000000000..eeecadafa --- /dev/null +++ b/x/lightclient/keeper/keeper.go @@ -0,0 +1,120 @@ +package keeper + +import ( + "bytes" + "fmt" + + "github.com/cometbft/cometbft/libs/log" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +type Keeper struct { + cdc codec.BinaryCodec + storeKey storetypes.StoreKey + ibcClientKeeper types.IBCClientKeeperExpected + sequencerKeeper types.SequencerKeeperExpected + rollappKeeper types.RollappKeeperExpected +} + +func NewKeeper( + cdc codec.BinaryCodec, + storeKey storetypes.StoreKey, + ibcKeeper types.IBCClientKeeperExpected, + sequencerKeeper types.SequencerKeeperExpected, + rollappKeeper types.RollappKeeperExpected, +) *Keeper { + k := &Keeper{ + cdc: cdc, + storeKey: storeKey, + ibcClientKeeper: ibcKeeper, + sequencerKeeper: sequencerKeeper, + rollappKeeper: rollappKeeper, + } + return k +} + +// GetSequencerHash returns the seqeuncer's tendermint public key hash +func (k Keeper) GetSequencerHash(ctx sdk.Context, sequencerAddr string) ([]byte, error) { + seq, found := k.sequencerKeeper.GetSequencer(ctx, sequencerAddr) + if !found { + return nil, fmt.Errorf("sequencer not found") + } + return seq.GetDymintPubKeyHash() +} + +func (k Keeper) GetSequencerPubKey(ctx sdk.Context, sequencerAddr string) (tmprotocrypto.PublicKey, error) { + seq, found := k.sequencerKeeper.GetSequencer(ctx, sequencerAddr) + if !found { + return tmprotocrypto.PublicKey{}, fmt.Errorf("sequencer not found") + } + return seq.GetCometPubKey() +} + +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +func (k Keeper) GetSequencerFromValHash(ctx sdk.Context, rollappID string, blockValHash []byte) (string, error) { + sequencerList := k.sequencerKeeper.GetSequencersByRollapp(ctx, rollappID) + for _, seq := range sequencerList { + seqHash, err := seq.GetDymintPubKeyHash() + if err != nil { + return "", err + } + if bytes.Equal(seqHash, blockValHash) { + return seq.Address, nil + } + } + return "", types.ErrSequencerNotFound +} + +// SetConsenusStateSigner sets block valHash for the given height of the client +func (k Keeper) SetConsensusStateValHash(ctx sdk.Context, clientID string, height uint64, blockValHash []byte) { + store := ctx.KVStore(k.storeKey) + store.Set(types.ConsensusStateValhashKeyByClientID(clientID, height), blockValHash) +} + +func (k Keeper) RemoveConsensusStateValHash(ctx sdk.Context, clientID string, height uint64) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.ConsensusStateValhashKeyByClientID(clientID, height)) +} + +// GetConsensusStateValHash returns the block valHash for the given height of the client +func (k Keeper) GetConsensusStateValHash(ctx sdk.Context, clientID string, height uint64) ([]byte, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.ConsensusStateValhashKeyByClientID(clientID, height)) + if bz == nil { + return nil, false + } + return bz, true +} + +func (k Keeper) GetAllConsensusStateSigners(ctx sdk.Context) (signers []types.ConsensusStateSigner) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.ConsensusStateValhashKey) + defer iterator.Close() // nolint: errcheck + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + clientID, height := types.ParseConsensusStateValhashKey(key) + signers = append(signers, types.ConsensusStateSigner{ + IbcClientId: clientID, + Height: height, + BlockValHash: string(iterator.Value()), + }) + } + return +} + +func (k Keeper) GetRollappForClientID(ctx sdk.Context, clientID string) (string, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.CanonicalClientKey(clientID)) + if bz == nil { + return "", false + } + return string(bz), true +} diff --git a/x/lightclient/module.go b/x/lightclient/module.go new file mode 100644 index 000000000..31fef91fa --- /dev/null +++ b/x/lightclient/module.go @@ -0,0 +1,148 @@ +package sequencer + +import ( + "encoding/json" + "fmt" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/dymensionxyz/dymension/v3/x/lightclient/keeper" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// ---------------------------------------------------------------------------- +// AppModuleBasic +// ---------------------------------------------------------------------------- + +// AppModuleBasic implements the AppModuleBasic interface for the module. +type AppModuleBasic struct { + cdc codec.BinaryCodec +} + +func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { + return AppModuleBasic{cdc: cdc} +} + +// Name returns the module's name. +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +func (AppModuleBasic) RegisterCodec(cdc *codec.LegacyAmino) { +} + +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { +} + +// RegisterInterfaces registers the module's interface types +func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { +} + +// DefaultGenesis returns the module's default genesis state. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + defaultGenesis := types.DefaultGenesisState() + return cdc.MustMarshalJSON(&defaultGenesis) +} + +// ValidateGenesis performs genesis state validation for the module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + var genState types.GenesisState + if err := cdc.UnmarshalJSON(bz, &genState); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + return genState.Validate() +} + +// RegisterRESTRoutes registers the module's REST service handlers. +func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, rtr *mux.Router) { +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { +} + +// GetTxCmd returns the module's root tx command. +func (a AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +// GetQueryCmd returns the module's root query command. +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// ---------------------------------------------------------------------------- +// AppModule +// ---------------------------------------------------------------------------- + +// AppModule implements the AppModule interface for the module. +type AppModule struct { + AppModuleBasic + + keeper keeper.Keeper +} + +func NewAppModule( + cdc codec.Codec, + keeper keeper.Keeper, +) AppModule { + return AppModule{ + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + } +} + +// Name returns the module's name. +func (am AppModule) Name() string { + return am.AppModuleBasic.Name() +} + +// RegisterServices registers a GRPC query service to respond to the +// module-specific GRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { +} + +// RegisterInvariants registers the module's invariants. +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 { + var genState types.GenesisState + cdc.MustUnmarshalJSON(gs, &genState) + am.keeper.InitGenesis(ctx, genState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the module's exported genesis state as raw JSON bytes. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + genState := am.keeper.ExportGenesis(ctx) + return cdc.MustMarshalJSON(&genState) +} + +// ConsensusVersion implements ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } + +// BeginBlock executes all ABCI BeginBlock logic respective to the module. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock executes all ABCI EndBlock logic respective to the module. It +// returns no validator updates. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/lightclient/types/errors.go b/x/lightclient/types/errors.go new file mode 100644 index 000000000..309fb6e1f --- /dev/null +++ b/x/lightclient/types/errors.go @@ -0,0 +1,13 @@ +package types + +import ( + errorsmod "cosmossdk.io/errors" + "github.com/dymensionxyz/gerr-cosmos/gerrc" +) + +var ( + ErrStateRootsMismatch = errorsmod.Wrap(gerrc.ErrFailedPrecondition, "block descriptor state root does not match tendermint header app hash") + ErrValidatorHashMismatch = errorsmod.Wrap(gerrc.ErrFailedPrecondition, "next validator hash does not match the sequencer for h+1") + ErrTimestampMismatch = errorsmod.Wrap(gerrc.ErrFailedPrecondition, "block descriptor timestamp does not match tendermint header timestamp") + ErrSequencerNotFound = errorsmod.Wrap(gerrc.ErrNotFound, "sequencer for given valhash not found") +) diff --git a/x/lightclient/types/expected_keepers.go b/x/lightclient/types/expected_keepers.go new file mode 100644 index 000000000..a014f7e99 --- /dev/null +++ b/x/lightclient/types/expected_keepers.go @@ -0,0 +1,36 @@ +package types + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + sequencertypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" +) + +type SequencerKeeperExpected interface { + GetSequencer(ctx sdk.Context, sequencerAddress string) (val sequencertypes.Sequencer, found bool) + GetSequencersByRollapp(ctx sdk.Context, rollappId string) (list []sequencertypes.Sequencer) +} + +type RollappKeeperExpected interface { + GetRollapp(ctx sdk.Context, rollappId string) (val rollapptypes.Rollapp, found bool) + FindStateInfoByHeight(ctx sdk.Context, rollappId string, height uint64) (*rollapptypes.StateInfo, error) + GetStateInfo(ctx sdk.Context, rollappId string, index uint64) (val rollapptypes.StateInfo, found bool) + SetRollapp(ctx sdk.Context, rollapp rollapptypes.Rollapp) + HandleFraud(ctx sdk.Context, rollappID, clientId string, fraudHeight uint64, seqAddr string) error +} + +type IBCClientKeeperExpected interface { + GetClientConsensusState(ctx sdk.Context, clientID string, height exported.Height) (exported.ConsensusState, bool) + GetClientState(ctx sdk.Context, clientID string) (exported.ClientState, bool) + IterateClientStates(ctx sdk.Context, prefix []byte, cb func(clientID string, cs exported.ClientState) bool) + ConsensusStateHeights(c context.Context, req *ibcclienttypes.QueryConsensusStateHeightsRequest) (*ibcclienttypes.QueryConsensusStateHeightsResponse, error) +} + +type IBCChannelKeeperExpected interface { + GetChannelConnection(ctx sdk.Context, portID, channelID string) (string, exported.ConnectionI, error) +} diff --git a/x/lightclient/types/genesis.go b/x/lightclient/types/genesis.go new file mode 100644 index 000000000..dc655184f --- /dev/null +++ b/x/lightclient/types/genesis.go @@ -0,0 +1,33 @@ +package types + +import fmt "fmt" + +func DefaultGenesisState() GenesisState { + return GenesisState{ + CanonicalClients: []CanonicalClient{}, + ConsensusStateSigners: []ConsensusStateSigner{}, + } +} + +func (g GenesisState) Validate() error { + for _, client := range g.CanonicalClients { + if client.RollappId == "" { + return fmt.Errorf("invalid rollapp id: %v", client) + } + if client.IbcClientId == "" { + return fmt.Errorf("invalid ibc client id: %v", client) + } + } + for _, stateSigner := range g.ConsensusStateSigners { + if stateSigner.IbcClientId == "" { + return fmt.Errorf("invalid ibc client id: %v", stateSigner) + } + if stateSigner.Height == 0 { + return fmt.Errorf("invalid height: %v", stateSigner) + } + if stateSigner.BlockValHash == "" { + return fmt.Errorf("invalid signer: %v", stateSigner) + } + } + return nil +} diff --git a/x/lightclient/types/genesis.pb.go b/x/lightclient/types/genesis.pb.go new file mode 100644 index 000000000..bac2bb4eb --- /dev/null +++ b/x/lightclient/types/genesis.pb.go @@ -0,0 +1,883 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: dymensionxyz/dymension/lightclient/genesis.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type GenesisState struct { + CanonicalClients []CanonicalClient `protobuf:"bytes,1,rep,name=canonical_clients,json=canonicalClients,proto3" json:"canonical_clients"` + ConsensusStateSigners []ConsensusStateSigner `protobuf:"bytes,2,rep,name=consensus_state_signers,json=consensusStateSigners,proto3" json:"consensus_state_signers"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_5520440548912168, []int{0} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetCanonicalClients() []CanonicalClient { + if m != nil { + return m.CanonicalClients + } + return nil +} + +func (m *GenesisState) GetConsensusStateSigners() []ConsensusStateSigner { + if m != nil { + return m.ConsensusStateSigners + } + return nil +} + +type CanonicalClient struct { + RollappId string `protobuf:"bytes,1,opt,name=rollapp_id,json=rollappId,proto3" json:"rollapp_id,omitempty"` + IbcClientId string `protobuf:"bytes,2,opt,name=ibc_client_id,json=ibcClientId,proto3" json:"ibc_client_id,omitempty"` +} + +func (m *CanonicalClient) Reset() { *m = CanonicalClient{} } +func (m *CanonicalClient) String() string { return proto.CompactTextString(m) } +func (*CanonicalClient) ProtoMessage() {} +func (*CanonicalClient) Descriptor() ([]byte, []int) { + return fileDescriptor_5520440548912168, []int{1} +} +func (m *CanonicalClient) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CanonicalClient) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CanonicalClient.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CanonicalClient) XXX_Merge(src proto.Message) { + xxx_messageInfo_CanonicalClient.Merge(m, src) +} +func (m *CanonicalClient) XXX_Size() int { + return m.Size() +} +func (m *CanonicalClient) XXX_DiscardUnknown() { + xxx_messageInfo_CanonicalClient.DiscardUnknown(m) +} + +var xxx_messageInfo_CanonicalClient proto.InternalMessageInfo + +func (m *CanonicalClient) GetRollappId() string { + if m != nil { + return m.RollappId + } + return "" +} + +func (m *CanonicalClient) GetIbcClientId() string { + if m != nil { + return m.IbcClientId + } + return "" +} + +type ConsensusStateSigner struct { + // ibc_client_id is the canonical IBC client which has accepted a client update optimistically + IbcClientId string `protobuf:"bytes,1,opt,name=ibc_client_id,json=ibcClientId,proto3" json:"ibc_client_id,omitempty"` + // height is the client height which was updated optimistically + Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"` + // blockValHash is the valhash of the block which was updated optimistically + BlockValHash string `protobuf:"bytes,3,opt,name=blockValHash,proto3" json:"blockValHash,omitempty"` +} + +func (m *ConsensusStateSigner) Reset() { *m = ConsensusStateSigner{} } +func (m *ConsensusStateSigner) String() string { return proto.CompactTextString(m) } +func (*ConsensusStateSigner) ProtoMessage() {} +func (*ConsensusStateSigner) Descriptor() ([]byte, []int) { + return fileDescriptor_5520440548912168, []int{2} +} +func (m *ConsensusStateSigner) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ConsensusStateSigner) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ConsensusStateSigner.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ConsensusStateSigner) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConsensusStateSigner.Merge(m, src) +} +func (m *ConsensusStateSigner) XXX_Size() int { + return m.Size() +} +func (m *ConsensusStateSigner) XXX_DiscardUnknown() { + xxx_messageInfo_ConsensusStateSigner.DiscardUnknown(m) +} + +var xxx_messageInfo_ConsensusStateSigner proto.InternalMessageInfo + +func (m *ConsensusStateSigner) GetIbcClientId() string { + if m != nil { + return m.IbcClientId + } + return "" +} + +func (m *ConsensusStateSigner) GetHeight() uint64 { + if m != nil { + return m.Height + } + return 0 +} + +func (m *ConsensusStateSigner) GetBlockValHash() string { + if m != nil { + return m.BlockValHash + } + return "" +} + +func init() { + proto.RegisterType((*GenesisState)(nil), "dymensionxyz.dymension.lightclient.GenesisState") + proto.RegisterType((*CanonicalClient)(nil), "dymensionxyz.dymension.lightclient.CanonicalClient") + proto.RegisterType((*ConsensusStateSigner)(nil), "dymensionxyz.dymension.lightclient.ConsensusStateSigner") +} + +func init() { + proto.RegisterFile("dymensionxyz/dymension/lightclient/genesis.proto", fileDescriptor_5520440548912168) +} + +var fileDescriptor_5520440548912168 = []byte{ + // 354 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xc1, 0x4e, 0xea, 0x40, + 0x14, 0x86, 0x3b, 0x40, 0x48, 0x18, 0xb8, 0xb9, 0xf7, 0x36, 0xa8, 0x8d, 0x89, 0x95, 0x74, 0xc5, + 0xaa, 0x35, 0xb2, 0x61, 0x0d, 0x0b, 0x65, 0x5b, 0x8c, 0x0b, 0x37, 0x4d, 0x3b, 0x1d, 0xdb, 0x89, + 0x65, 0xa6, 0xe1, 0x0c, 0x04, 0x7c, 0x0a, 0x1f, 0x8b, 0x25, 0x4b, 0x57, 0xc6, 0xc0, 0xde, 0x67, + 0x30, 0x9d, 0x16, 0x02, 0x82, 0xd1, 0xdd, 0xfc, 0x67, 0xce, 0xf7, 0xff, 0x67, 0x26, 0x07, 0x5f, + 0x85, 0xf3, 0x11, 0xe5, 0xc0, 0x04, 0x9f, 0xcd, 0x9f, 0x9d, 0xad, 0x70, 0x12, 0x16, 0xc5, 0x92, + 0x24, 0x8c, 0x72, 0xe9, 0x44, 0x94, 0x53, 0x60, 0x60, 0xa7, 0x63, 0x21, 0x85, 0x6e, 0xed, 0x12, + 0xf6, 0x56, 0xd8, 0x3b, 0xc4, 0x79, 0x33, 0x12, 0x91, 0x50, 0xed, 0x4e, 0x76, 0xca, 0x49, 0xeb, + 0x03, 0xe1, 0xc6, 0x4d, 0xee, 0x35, 0x94, 0xbe, 0xa4, 0xfa, 0x23, 0xfe, 0x4f, 0x7c, 0x2e, 0x38, + 0x23, 0x7e, 0xe2, 0xe5, 0x28, 0x18, 0xa8, 0x55, 0x6e, 0xd7, 0xaf, 0x3b, 0xf6, 0xcf, 0x31, 0x76, + 0x7f, 0x03, 0xf7, 0x95, 0xee, 0x55, 0x16, 0x6f, 0x97, 0x9a, 0xfb, 0x8f, 0xec, 0x97, 0x41, 0x9f, + 0xe2, 0x33, 0x22, 0x38, 0x50, 0x0e, 0x13, 0xf0, 0x20, 0x8b, 0xf6, 0x80, 0x45, 0x9c, 0x8e, 0xc1, + 0x28, 0xa9, 0xb4, 0xee, 0xaf, 0xd2, 0x36, 0x16, 0x6a, 0xf8, 0xa1, 0x32, 0x28, 0x22, 0x4f, 0xc8, + 0x91, 0x3b, 0xb0, 0xee, 0xf0, 0xdf, 0x2f, 0x23, 0xea, 0x17, 0x18, 0x8f, 0x45, 0x92, 0xf8, 0x69, + 0xea, 0xb1, 0xd0, 0x40, 0x2d, 0xd4, 0xae, 0xb9, 0xb5, 0xa2, 0x32, 0x08, 0x75, 0x0b, 0xff, 0x61, + 0x01, 0x29, 0xfe, 0x22, 0xeb, 0x28, 0xa9, 0x8e, 0x3a, 0x0b, 0x48, 0x6e, 0x30, 0x08, 0xad, 0x29, + 0x6e, 0x1e, 0x1b, 0xe5, 0x90, 0x45, 0x07, 0xac, 0x7e, 0x8a, 0xab, 0x31, 0xcd, 0xde, 0xa4, 0x8c, + 0x2b, 0x6e, 0xa1, 0x74, 0x0b, 0x37, 0x82, 0x44, 0x90, 0xa7, 0x7b, 0x3f, 0xb9, 0xf5, 0x21, 0x36, + 0xca, 0x0a, 0xdd, 0xab, 0xf5, 0xdc, 0xc5, 0xca, 0x44, 0xcb, 0x95, 0x89, 0xde, 0x57, 0x26, 0x7a, + 0x59, 0x9b, 0xda, 0x72, 0x6d, 0x6a, 0xaf, 0x6b, 0x53, 0x7b, 0xe8, 0x46, 0x4c, 0xc6, 0x93, 0xc0, + 0x26, 0x62, 0xe4, 0x7c, 0xb3, 0x4f, 0xd3, 0x8e, 0x33, 0xdb, 0x5b, 0x2a, 0x39, 0x4f, 0x29, 0x04, + 0x55, 0xb5, 0x19, 0x9d, 0xcf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x49, 0x76, 0xae, 0x91, 0x87, 0x02, + 0x00, 0x00, +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ConsensusStateSigners) > 0 { + for iNdEx := len(m.ConsensusStateSigners) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ConsensusStateSigners[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.CanonicalClients) > 0 { + for iNdEx := len(m.CanonicalClients) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.CanonicalClients[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *CanonicalClient) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CanonicalClient) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CanonicalClient) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.IbcClientId) > 0 { + i -= len(m.IbcClientId) + copy(dAtA[i:], m.IbcClientId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.IbcClientId))) + i-- + dAtA[i] = 0x12 + } + if len(m.RollappId) > 0 { + i -= len(m.RollappId) + copy(dAtA[i:], m.RollappId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.RollappId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ConsensusStateSigner) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ConsensusStateSigner) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ConsensusStateSigner) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.BlockValHash) > 0 { + i -= len(m.BlockValHash) + copy(dAtA[i:], m.BlockValHash) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.BlockValHash))) + i-- + dAtA[i] = 0x1a + } + if m.Height != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x10 + } + if len(m.IbcClientId) > 0 { + i -= len(m.IbcClientId) + copy(dAtA[i:], m.IbcClientId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.IbcClientId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.CanonicalClients) > 0 { + for _, e := range m.CanonicalClients { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.ConsensusStateSigners) > 0 { + for _, e := range m.ConsensusStateSigners { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *CanonicalClient) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.RollappId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.IbcClientId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + return n +} + +func (m *ConsensusStateSigner) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.IbcClientId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + if m.Height != 0 { + n += 1 + sovGenesis(uint64(m.Height)) + } + l = len(m.BlockValHash) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CanonicalClients", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.CanonicalClients = append(m.CanonicalClients, CanonicalClient{}) + if err := m.CanonicalClients[len(m.CanonicalClients)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsensusStateSigners", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ConsensusStateSigners = append(m.ConsensusStateSigners, ConsensusStateSigner{}) + if err := m.ConsensusStateSigners[len(m.ConsensusStateSigners)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CanonicalClient) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CanonicalClient: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CanonicalClient: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RollappId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RollappId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field IbcClientId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.IbcClientId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ConsensusStateSigner) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ConsensusStateSigner: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ConsensusStateSigner: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field IbcClientId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.IbcClientId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockValHash", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BlockValHash = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/lightclient/types/genesis_test.go b/x/lightclient/types/genesis_test.go new file mode 100644 index 000000000..a0c3a9317 --- /dev/null +++ b/x/lightclient/types/genesis_test.go @@ -0,0 +1,86 @@ +package types_test + +import ( + "testing" + + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + "github.com/stretchr/testify/require" +) + +func TestGenesisValidate(t *testing.T) { + tests := []struct { + name string + g types.GenesisState + valid bool + }{ + { + name: "valid", + g: types.GenesisState{ + CanonicalClients: []types.CanonicalClient{ + {RollappId: "rollapp-1", IbcClientId: "client-1"}, + {RollappId: "rollapp-2", IbcClientId: "client-2"}, + }, + ConsensusStateSigners: []types.ConsensusStateSigner{ + {IbcClientId: "client-1", Height: 1, BlockValHash: "signer-1"}, + {IbcClientId: "client-1", Height: 2, BlockValHash: "signer-1"}, + }, + }, + valid: true, + }, + { + name: "invalid rollapp id", + g: types.GenesisState{ + CanonicalClients: []types.CanonicalClient{ + {RollappId: "", IbcClientId: "client-1"}, + }, + }, + valid: false, + }, + { + name: "invalid ibc client id", + g: types.GenesisState{ + CanonicalClients: []types.CanonicalClient{ + {RollappId: "rollapp-1", IbcClientId: ""}, + }, + }, + valid: false, + }, + { + name: "invalid height", + g: types.GenesisState{ + ConsensusStateSigners: []types.ConsensusStateSigner{ + {IbcClientId: "client-1", Height: 0, BlockValHash: "signer-1"}, + }, + }, + valid: false, + }, + { + name: "invalid blockvalhash", + g: types.GenesisState{ + ConsensusStateSigners: []types.ConsensusStateSigner{ + {IbcClientId: "client-1", Height: 1, BlockValHash: ""}, + }, + }, + valid: false, + }, + { + name: "empty", + g: types.GenesisState{}, + valid: true, + }, + { + name: "default", + g: types.DefaultGenesisState(), + valid: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.valid { + require.NoError(t, tt.g.Validate()) + } else { + require.Error(t, tt.g.Validate()) + } + }) + } +} diff --git a/x/lightclient/types/keys.go b/x/lightclient/types/keys.go new file mode 100644 index 000000000..3ad12c0ad --- /dev/null +++ b/x/lightclient/types/keys.go @@ -0,0 +1,53 @@ +package types + +import ( + "bytes" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName defines the module name + ModuleName = "lightclient" + + // StoreKey defines the primary module store key + StoreKey = ModuleName +) + +const ( + keySeparator = "/" +) + +var ( + RollappClientKey = []byte{0x01} + ConsensusStateValhashKey = []byte{0x03} + canonicalClientKey = []byte{0x04} +) + +func GetRollappClientKey(rollappId string) []byte { + key := RollappClientKey + key = append(key, []byte(rollappId)...) + return key +} + +func ConsensusStateValhashKeyByClientID(clientID string, height uint64) []byte { + key := ConsensusStateValhashKey + key = append(key, []byte(clientID)...) + key = append(key, keySeparator...) + key = append(key, sdk.Uint64ToBigEndian(height)...) + return key +} + +func CanonicalClientKey(clientID string) []byte { + key := canonicalClientKey + key = append(key, []byte(clientID)...) + return key +} + +func ParseConsensusStateValhashKey(key []byte) (clientID string, height uint64) { + key = key[len(ConsensusStateValhashKey):] + parts := bytes.Split(key, []byte(keySeparator)) + clientID = string(parts[0]) + height = sdk.BigEndianToUint64(parts[1]) + return +} diff --git a/x/lightclient/types/params.go b/x/lightclient/types/params.go new file mode 100644 index 000000000..1daeda364 --- /dev/null +++ b/x/lightclient/types/params.go @@ -0,0 +1,129 @@ +package types + +import ( + "bytes" + "time" + + "github.com/cometbft/cometbft/libs/math" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + ics23 "github.com/cosmos/ics23/go" +) + +// ExpectedCanonicalClientParams defines the expected parameters for a canonical IBC Tendermint client state +// The ChainID is not included as that varies for each rollapp +// The LatestHeight is not included as there is no condition on when a client can be registered as canonical +var ExpectedCanonicalClientParams = ibctm.ClientState{ + // Trust level is the fraction of the trusted validator set + // that must sign over a new untrusted header before it is accepted. + // For a rollapp should be 1/1. + TrustLevel: ibctm.NewFractionFromTm(math.Fraction{Numerator: 1, Denominator: 1}), + // TrustingPeriod is the duration of the period since the + // LatestTimestamp during which the submitted headers are valid for update. + TrustingPeriod: time.Hour * 24 * 7 * 2, + // Unbonding period is the duration of the sequencer unbonding period. + UnbondingPeriod: time.Hour * 24 * 7 * 3, + // MaxClockDrift defines how much new (untrusted) header's Time + // can drift into the future relative to our local clock. + MaxClockDrift: time.Minute * 10, + // Frozen Height should be zero (default) as frozen clients cannot be canonical + // as they cannot receive state updates + FrozenHeight: ibcclienttypes.ZeroHeight(), + // ProofSpecs defines the ICS-23 standard proof specifications used by + // the light client. It is used configure a proof for either existence + // or non-existence of a key value pair + ProofSpecs: commitmenttypes.GetSDKSpecs(), + AllowUpdateAfterExpiry: false, + AllowUpdateAfterMisbehaviour: false, + // For chains using Cosmos-SDK's default x/upgrade module, the upgrade path is as follows + UpgradePath: []string{"upgrade", "upgradedIBCState"}, +} + +// IsCanonicalClientParamsValid checks if the given IBC tendermint client state has the expected canonical client parameters +func IsCanonicalClientParamsValid(clientState *ibctm.ClientState) bool { + if clientState.TrustLevel != ExpectedCanonicalClientParams.TrustLevel { + return false + } + if clientState.TrustingPeriod != ExpectedCanonicalClientParams.TrustingPeriod { + return false + } + if clientState.UnbondingPeriod != ExpectedCanonicalClientParams.UnbondingPeriod { + return false + } + if clientState.MaxClockDrift != ExpectedCanonicalClientParams.MaxClockDrift { + return false + } + if clientState.FrozenHeight != ExpectedCanonicalClientParams.FrozenHeight { + return false + } + if clientState.AllowUpdateAfterExpiry != ExpectedCanonicalClientParams.AllowUpdateAfterExpiry { + return false + } + if clientState.AllowUpdateAfterMisbehaviour != ExpectedCanonicalClientParams.AllowUpdateAfterMisbehaviour { + return false + } + for i, proofSpec := range clientState.ProofSpecs { + if !EqualICS23ProofSpecs(*proofSpec, *ExpectedCanonicalClientParams.ProofSpecs[i]) { + return false + } + } + for i, path := range clientState.UpgradePath { + if path != ExpectedCanonicalClientParams.UpgradePath[i] { + return false + } + } + return true +} + +func EqualICS23ProofSpecs(proofSpecs1, proofSpecs2 ics23.ProofSpec) bool { + if proofSpecs1.MaxDepth != proofSpecs2.MaxDepth { + return false + } + if proofSpecs1.MinDepth != proofSpecs2.MinDepth { + return false + } + if proofSpecs1.PrehashKeyBeforeComparison != proofSpecs2.PrehashKeyBeforeComparison { + return false + } + if proofSpecs1.LeafSpec.Hash != proofSpecs2.LeafSpec.Hash { + return false + } + if proofSpecs1.LeafSpec.PrehashKey != proofSpecs2.LeafSpec.PrehashKey { + return false + } + if proofSpecs1.LeafSpec.PrehashValue != proofSpecs2.LeafSpec.PrehashValue { + return false + } + if proofSpecs1.LeafSpec.Length != proofSpecs2.LeafSpec.Length { + return false + } + if !bytes.Equal(proofSpecs1.LeafSpec.Prefix, proofSpecs2.LeafSpec.Prefix) { + return false + } + if len(proofSpecs1.InnerSpec.ChildOrder) != len(proofSpecs2.InnerSpec.ChildOrder) { + return false + } + for i, childOrder := range proofSpecs1.InnerSpec.ChildOrder { + if childOrder != proofSpecs2.InnerSpec.ChildOrder[i] { + return false + } + } + if proofSpecs1.InnerSpec.ChildSize != proofSpecs2.InnerSpec.ChildSize { + return false + } + if proofSpecs1.InnerSpec.MinPrefixLength != proofSpecs2.InnerSpec.MinPrefixLength { + return false + } + if proofSpecs1.InnerSpec.MaxPrefixLength != proofSpecs2.InnerSpec.MaxPrefixLength { + return false + } + if !bytes.Equal(proofSpecs1.InnerSpec.EmptyChild, proofSpecs2.InnerSpec.EmptyChild) { + return false + } + if proofSpecs1.InnerSpec.Hash != proofSpecs2.InnerSpec.Hash { + return false + } + + return true +} diff --git a/x/lightclient/types/params_test.go b/x/lightclient/types/params_test.go new file mode 100644 index 000000000..38e31344d --- /dev/null +++ b/x/lightclient/types/params_test.go @@ -0,0 +1,117 @@ +package types_test + +import ( + "testing" + + "github.com/cometbft/cometbft/libs/math" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + ics23 "github.com/cosmos/ics23/go" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" +) + +func TestIsCanonicalClientParamsValid(t *testing.T) { + testCases := []struct { + name string + clientState func() ibctm.ClientState + valid bool + }{ + { + "valid client state", + func() ibctm.ClientState { + return types.ExpectedCanonicalClientParams + }, + true, + }, + { + "invalid trust level", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.TrustLevel = ibctm.NewFractionFromTm(math.Fraction{Numerator: 1, Denominator: 2}) + return clientState + }, + false, + }, + { + "invalid trusting period", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.TrustingPeriod = clientState.TrustingPeriod + 1 + return clientState + }, + false, + }, + { + "invalid unbonding period", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.UnbondingPeriod = clientState.UnbondingPeriod + 1 + return clientState + }, + false, + }, + { + "invalid max clock drift", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.MaxClockDrift = clientState.MaxClockDrift + 1 + return clientState + }, + false, + }, + { + "invalid frozen height", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.FrozenHeight = ibcclienttypes.NewHeight(1, 1) + return clientState + }, + false, + }, + { + "invalid allow update after expiry", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.AllowUpdateAfterExpiry = true + return clientState + }, + false, + }, + { + "invalid allow update after misbehaviour", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.AllowUpdateAfterMisbehaviour = true + return clientState + }, + false, + }, + { + "invalid proof specs", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.ProofSpecs = []*ics23.ProofSpec{ics23.SmtSpec} + return clientState + }, + false, + }, + { + "invalid upgrade path", + func() ibctm.ClientState { + clientState := types.ExpectedCanonicalClientParams + clientState.UpgradePath = []string{"custom", "upgrade"} + return clientState + }, + false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + clientState := tc.clientState() + valid := types.IsCanonicalClientParamsValid(&clientState) + if valid != tc.valid { + t.Errorf("expected valid: %v, got: %v", tc.valid, valid) + } + }) + } +} diff --git a/x/lightclient/types/state.go b/x/lightclient/types/state.go new file mode 100644 index 000000000..5f5a850d5 --- /dev/null +++ b/x/lightclient/types/state.go @@ -0,0 +1,60 @@ +package types + +import ( + "bytes" + "errors" + + errorsmod "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + cmttypes "github.com/cometbft/cometbft/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" +) + +// CheckCompatibility checks if the IBC state and Rollapp state are compatible +// Compatibility Criteria: +// 1. The app root shared by the IBC consensus state matches the block descriptor state root for the same height +// 2. The next validator hash shared by the IBC consensus state matches the sequencer hash for the next block descriptor +// 3. The block descriptor timestamp matches the tendermint header timestamp (only if timestamp exists for the block descriptor) +func CheckCompatibility(ibcState ibctm.ConsensusState, raState RollappState) error { + // Check if block descriptor state root matches IBC block header app hash + if !bytes.Equal(ibcState.Root.GetHash(), raState.BlockDescriptor.StateRoot) { + return errorsmod.Wrap(ErrStateRootsMismatch, "block descriptor state root does not match tendermint header app hash") + } + // Check if the nextValidatorHash matches for the sequencer for h+1 block descriptor + nextValHashFromStateInfo, err := GetValHashForSequencer(raState.NextBlockSequencer) + if err != nil { + return errors.Join(ErrValidatorHashMismatch, err) + } + if !bytes.Equal(ibcState.NextValidatorsHash, nextValHashFromStateInfo) { + return errorsmod.Wrap(ErrValidatorHashMismatch, "next validator hash does not match the sequencer for h+1") + } + if !raState.BlockDescriptor.Timestamp.IsZero() && !ibcState.Timestamp.Equal(raState.BlockDescriptor.Timestamp) { + return errorsmod.Wrap(ErrTimestampMismatch, "block descriptor timestamp does not match tendermint header timestamp") + } + return nil +} + +// GetValHashForSequencer creates a dummy tendermint validatorset to +// calculate the nextValHash for the sequencer and returns it +func GetValHashForSequencer(sequencerTmPubKey tmprotocrypto.PublicKey) ([]byte, error) { + var nextValSet cmttypes.ValidatorSet + updates, err := cmttypes.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{{Power: 1, PubKey: sequencerTmPubKey}}) + if err != nil { + return nil, err + } + err = nextValSet.UpdateWithChangeSet(updates) + if err != nil { + return nil, err + } + return nextValSet.Hash(), nil +} + +type RollappState struct { + // BlockDescriptor is the block descriptor for the required height + BlockDescriptor rollapptypes.BlockDescriptor + // NextBlockSequencer is the tendermint pubkey of the sequencer who submitted the block descriptor for the next height (h+1) + NextBlockSequencer tmprotocrypto.PublicKey +} diff --git a/x/lightclient/types/state_test.go b/x/lightclient/types/state_test.go new file mode 100644 index 000000000..9c4cefa83 --- /dev/null +++ b/x/lightclient/types/state_test.go @@ -0,0 +1,116 @@ +package types_test + +import ( + "testing" + "time" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + commitmenttypes "github.com/cosmos/ibc-go/v7/modules/core/23-commitment/types" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + "github.com/dymensionxyz/dymension/v3/x/lightclient/types" + rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/stretchr/testify/require" +) + +var ( + sequencerPubKey = ed25519.GenPrivKey().PubKey() + tmPk, _ = cryptocodec.ToTmProtoPublicKey(sequencerPubKey) + valHash, _ = types.GetValHashForSequencer(tmPk) + timestamp = time.Unix(1724392989, 0) + + validIBCState = ibctm.ConsensusState{ + Root: commitmenttypes.NewMerkleRoot([]byte("root")), + Timestamp: timestamp, + NextValidatorsHash: valHash, + } + validRollappState = types.RollappState{ + BlockDescriptor: rollapptypes.BlockDescriptor{ + StateRoot: []byte("root"), + Timestamp: timestamp, + }, + NextBlockSequencer: tmPk, + } +) + +func TestCheckCompatibility(t *testing.T) { + type input struct { + ibcState ibctm.ConsensusState + raState types.RollappState + } + testCases := []struct { + name string + input func() input + err error + }{ + { + name: "roots are not equal", + input: func() input { + invalidRootRaState := validRollappState + invalidRootRaState.BlockDescriptor.StateRoot = []byte("not same root") + return input{ + ibcState: validIBCState, + raState: invalidRootRaState, + } + }, + err: types.ErrStateRootsMismatch, + }, + { + name: "nextValidatorHash does not match the sequencer who submitted the next block descriptor", + input: func() input { + invalidNextValidatorHashIBCState := validIBCState + invalidNextValidatorHashIBCState.NextValidatorsHash = []byte("wrong next validator hash") + return input{ + ibcState: invalidNextValidatorHashIBCState, + raState: validRollappState, + } + }, + err: types.ErrValidatorHashMismatch, + }, + { + name: "timestamps is empty. ignore timestamp check", + input: func() input { + emptyTimestampRAState := validRollappState + emptyTimestampRAState.BlockDescriptor.Timestamp = time.Time{} + return input{ + ibcState: validIBCState, + raState: emptyTimestampRAState, + } + }, + err: nil, + }, + { + name: "timestamps are not equal", + input: func() input { + invalidTimestampRAState := validRollappState + invalidTimestampRAState.BlockDescriptor.Timestamp = timestamp.Add(1) + return input{ + ibcState: validIBCState, + raState: invalidTimestampRAState, + } + }, + err: types.ErrTimestampMismatch, + }, + { + name: "all fields are compatible", + input: func() input { + return input{ + ibcState: validIBCState, + raState: validRollappState, + } + }, + err: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := tc.input() + err := types.CheckCompatibility(input.ibcState, input.raState) + if err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/rollapp/keeper/grpc_query_get_state_info_by_height_test.go b/x/rollapp/keeper/grpc_query_get_state_info_by_height_test.go index 14059aac3..fa3dc0e27 100644 --- a/x/rollapp/keeper/grpc_query_get_state_info_by_height_test.go +++ b/x/rollapp/keeper/grpc_query_get_state_info_by_height_test.go @@ -88,9 +88,10 @@ func TestStateInfoByHeightMissingStateInfo(t *testing.T) { Height: 100, } _, err := k.StateInfo(wctx, request) + errIndex := 1 + (85-1)/2 // Using binary search, the middle index is lookedup first and is missing. require.EqualError(t, err, errorsmod.Wrapf(types.ErrNotFound, "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, 85).Error()) + rollappId, errIndex).Error()) } func TestStateInfoByHeightMissingStateInfo1(t *testing.T) { @@ -115,9 +116,10 @@ func TestStateInfoByHeightMissingStateInfo1(t *testing.T) { NumBlocks: 1, }) _, err := k.StateInfo(wctx, request) + errIndex := 1 + (60-1)/2 // Using binary search, the middle index is lookedup first and is missing. require.EqualError(t, err, errorsmod.Wrapf(types.ErrNotFound, "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, 1).Error()) + rollappId, errIndex).Error()) } func TestStateInfoByHeightErr(t *testing.T) { diff --git a/x/rollapp/keeper/grpc_query_state_info.go b/x/rollapp/keeper/grpc_query_state_info.go index 1cbf1f248..a6bdb00eb 100644 --- a/x/rollapp/keeper/grpc_query_state_info.go +++ b/x/rollapp/keeper/grpc_query_state_info.go @@ -6,6 +6,7 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/dymensionxyz/gerr-cosmos/gerrc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -73,111 +74,22 @@ func (k Keeper) FindStateInfoByHeight(ctx sdk.Context, rollappId string, height rollappId) } // initial interval to search in - startInfoIndex := uint64(1) // see TODO bellow + startInfoIndex := uint64(1) endInfoIndex := stateInfoIndex.Index - - // get state info - LatestStateInfo, found := k.GetStateInfo(ctx, rollappId, endInfoIndex) - if !found { - return nil, errorsmod.Wrapf(types.ErrNotFound, - "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, endInfoIndex) - } - - // check that height exists - if height >= LatestStateInfo.StartHeight+LatestStateInfo.NumBlocks { - return nil, errorsmod.Wrapf(types.ErrStateNotExists, - "rollappId=%s, height=%d", - rollappId, height) - } - - // check if the height belongs to this batch - if height >= LatestStateInfo.StartHeight { - return &LatestStateInfo, nil - } - - maxNumberOfSteps := endInfoIndex - startInfoIndex + 1 - stepNum := uint64(0) - for ; stepNum < maxNumberOfSteps; stepNum += 1 { - // we know that endInfoIndex > startInfoIndex - // otherwise the height should have been found - if endInfoIndex <= startInfoIndex { - return nil, errorsmod.Wrapf(types.ErrLogic, - "endInfoIndex should be != than startInfoIndex rollappId=%s, startInfoIndex=%d, endInfoIndex=%d", - rollappId, startInfoIndex, endInfoIndex) - } - // 1. get state info - startStateInfo, found := k.GetStateInfo(ctx, rollappId, startInfoIndex) - if !found { - // TODO: - // if stateInfo is missing it won't be logic error if history deletion be implemented - // for that we will have to check the oldest we have - return nil, errorsmod.Wrapf(types.ErrNotFound, - "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, startInfoIndex) + for startInfoIndex <= endInfoIndex { + midIndex := startInfoIndex + (endInfoIndex-startInfoIndex)/2 + state, ok := k.GetStateInfo(ctx, rollappId, midIndex) + if !ok { + return nil, errorsmod.Wrapf(gerrc.ErrNotFound, "StateInfo wasn't found for rollappId=%s, index=%d", rollappId, midIndex) } - endStateInfo, found := k.GetStateInfo(ctx, rollappId, endInfoIndex) - if !found { - return nil, errorsmod.Wrapf(types.ErrNotFound, - "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, endInfoIndex) - } - startHeight := startStateInfo.StartHeight - endHeight := endStateInfo.StartHeight + endStateInfo.NumBlocks - 1 - - // 2. check startStateInfo - if height >= startStateInfo.StartHeight && - (startStateInfo.StartHeight+startStateInfo.NumBlocks) > height { - return &startStateInfo, nil + if state.ContainsHeight(height) { + return &state, nil } - - // 3. check endStateInfo - if height >= endStateInfo.StartHeight && - (endStateInfo.StartHeight+endStateInfo.NumBlocks) > height { - return &endStateInfo, nil - } - - // 4. calculate the average blocks per batch - avgBlocksPerBatch := (endHeight - startHeight + 1) / (endInfoIndex - startInfoIndex + 1) - if avgBlocksPerBatch == 0 { - return nil, errorsmod.Wrapf(types.ErrLogic, - "avgBlocksPerBatch is zero!!! rollappId=%s, endHeight=%d, startHeight=%d, endInfoIndex=%d, startInfoIndex=%d", - rollappId, endHeight, startHeight, endInfoIndex, startInfoIndex) - } - - // 5. load the candidate block batch - infoIndexStep := (height - startHeight) / avgBlocksPerBatch - if infoIndexStep == 0 { - infoIndexStep = 1 - } - candidateInfoIndex := startInfoIndex + infoIndexStep - if candidateInfoIndex > endInfoIndex { - // skip to the last, probably the steps to big - candidateInfoIndex = endInfoIndex - } - if candidateInfoIndex == endInfoIndex { - candidateInfoIndex = endInfoIndex - 1 - } - candidateStateInfo, found := k.GetStateInfo(ctx, rollappId, candidateInfoIndex) - if !found { - return nil, errorsmod.Wrapf(types.ErrNotFound, - "StateInfo wasn't found for rollappId=%s, index=%d", - rollappId, candidateInfoIndex) - } - - // 6. check the candidate - if candidateStateInfo.StartHeight > height { - endInfoIndex = candidateInfoIndex - 1 + if height < state.GetStartHeight() { + endInfoIndex = midIndex - 1 } else { - if candidateStateInfo.StartHeight+candidateStateInfo.NumBlocks-1 < height { - startInfoIndex = candidateInfoIndex + 1 - } else { - return &candidateStateInfo, nil - } + startInfoIndex = midIndex + 1 } } - - return nil, errorsmod.Wrapf(types.ErrLogic, - "More searching steps than indexes! rollappId=%s, stepNum=%d, maxNumberOfSteps=%d", - rollappId, stepNum, maxNumberOfSteps) + return nil, errorsmod.Wrapf(types.ErrStateNotExists, "StateInfo wasn't found for rollappId=%s, height=%d", rollappId, height) } diff --git a/x/rollapp/keeper/grpc_query_state_info_test.go b/x/rollapp/keeper/grpc_query_state_info_test.go index 02f4aee9d..fb4a18ed6 100644 --- a/x/rollapp/keeper/grpc_query_state_info_test.go +++ b/x/rollapp/keeper/grpc_query_state_info_test.go @@ -12,6 +12,7 @@ import ( keepertest "github.com/dymensionxyz/dymension/v3/testutil/keeper" "github.com/dymensionxyz/dymension/v3/testutil/nullify" "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/dymensionxyz/sdk-utils/utils/urand" ) // Prevent strconv unused error @@ -78,3 +79,102 @@ func TestStateInfoQuerySingle(t *testing.T) { }) } } + +func TestFindStateInfoByHeight(t *testing.T) { + keeper, ctx := keepertest.RollappKeeper(t) + rollappID := urand.RollappID() + keeper.SetRollapp(ctx, types.Rollapp{ + RollappId: rollappID, + }) + keeper.SetStateInfo(ctx, types.StateInfo{ + StateInfoIndex: types.StateInfoIndex{RollappId: rollappID, Index: 1}, + StartHeight: 1, + NumBlocks: 2, + }) + keeper.SetStateInfo(ctx, types.StateInfo{ + StateInfoIndex: types.StateInfoIndex{RollappId: rollappID, Index: 2}, + StartHeight: 3, + NumBlocks: 3, + }) + keeper.SetStateInfo(ctx, types.StateInfo{ + StateInfoIndex: types.StateInfoIndex{RollappId: rollappID, Index: 3}, + StartHeight: 6, + NumBlocks: 4, + }) + keeper.SetLatestStateInfoIndex(ctx, types.StateInfoIndex{ + RollappId: rollappID, + Index: 3, + }) + + type testInput struct { + rollappId string + height uint64 + } + + testCase := []struct { + name string + input testInput + stateInfoIndex uint64 + err error + }{ + { + name: "Zero height", + input: testInput{ + rollappId: "1", + height: 0, + }, + err: types.ErrInvalidHeight, + }, + { + name: "Rollapp not found", + input: testInput{ + rollappId: "unknown", + height: 1, + }, + err: types.ErrUnknownRollappID, + }, + { + name: "First height", + input: testInput{ + rollappId: rollappID, + height: 1, + }, + stateInfoIndex: 1, + }, + { + name: "Last height", + input: testInput{ + rollappId: rollappID, + height: 9, + }, + stateInfoIndex: 3, + }, + { + name: "Height in between", + input: testInput{ + rollappId: rollappID, + height: 4, + }, + stateInfoIndex: 2, + }, + { + name: "Height not found", + input: testInput{ + rollappId: rollappID, + height: 10, + }, + err: types.ErrStateNotExists, + }, + } + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + response, err := keeper.FindStateInfoByHeight(ctx, tc.input.rollappId, tc.input.height) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + } else { + require.NoError(t, err) + require.Equal(t, tc.stateInfoIndex, response.StateInfoIndex.Index) + } + }) + } +} diff --git a/x/rollapp/keeper/msg_server_update_state.go b/x/rollapp/keeper/msg_server_update_state.go index 7a10230bc..f6f5e9698 100644 --- a/x/rollapp/keeper/msg_server_update_state.go +++ b/x/rollapp/keeper/msg_server_update_state.go @@ -41,6 +41,16 @@ func (k msgServer) UpdateState(goCtx context.Context, msg *types.MsgUpdateState) latestStateInfoIndex.Index, msg.RollappId) } + // if previous block descriptor has timestamp, it means the rollapp is upgraded + // therefore all new BDs need to have timestamp + lastBD := stateInfo.GetLatestBlockDescriptor() + if !lastBD.Timestamp.IsZero() { + err := msg.BDs.Validate() + if err != nil { + return nil, err + } + } + // check to see if received height is the one we expected expectedStartHeight := stateInfo.StartHeight + stateInfo.NumBlocks if expectedStartHeight != msg.StartHeight { @@ -51,6 +61,11 @@ func (k msgServer) UpdateState(goCtx context.Context, msg *types.MsgUpdateState) // bump state index lastIndex = latestStateInfoIndex.Index + } else { + err := msg.BDs.Validate() + if err != nil { + return nil, err + } } newIndex = lastIndex + 1 @@ -65,6 +80,11 @@ func (k msgServer) UpdateState(goCtx context.Context, msg *types.MsgUpdateState) // Write new state information to the store indexed by k.SetStateInfo(ctx, *stateInfo) + err = k.hooks.AfterUpdateState(ctx, msg.RollappId, stateInfo) + if err != nil { + return nil, err + } + stateInfoIndex := stateInfo.GetIndex() newFinalizationQueue := []types.StateInfoIndex{stateInfoIndex} diff --git a/x/rollapp/keeper/msg_server_update_state_test.go b/x/rollapp/keeper/msg_server_update_state_test.go index c2448b0db..f6992750a 100644 --- a/x/rollapp/keeper/msg_server_update_state_test.go +++ b/x/rollapp/keeper/msg_server_update_state_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "time" + abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/query" @@ -231,6 +233,45 @@ func (suite *RollappTestSuite) TestUpdateStateErrNotActiveSequencer() { suite.ErrorIs(err, sequencertypes.ErrNotActiveSequencer) } +func (suite *RollappTestSuite) TestUpdateStateDowngradeTimestamp() { + rollappId, proposer := suite.CreateDefaultRollappAndProposer() + // update state without timestamp + stateInfo := types.StateInfo{ + StateInfoIndex: types.StateInfoIndex{RollappId: rollappId, Index: 1}, + Sequencer: proposer, + StartHeight: 1, + NumBlocks: 1, + DAPath: "", + BDs: types.BlockDescriptors{BD: []types.BlockDescriptor{{Height: 1}}}, + } + suite.App.RollappKeeper.SetLatestStateInfoIndex(suite.Ctx, stateInfo.StateInfoIndex) + suite.App.RollappKeeper.SetStateInfo(suite.Ctx, stateInfo) + + // update state with timestamp - this "upgrades" the rollapp such that all new state updates must have timestamp in BD + updateState := types.MsgUpdateState{ + Creator: proposer, + RollappId: rollappId, + StartHeight: 2, + NumBlocks: 1, + DAPath: "", + BDs: types.BlockDescriptors{BD: []types.BlockDescriptor{{Height: 2, Timestamp: time.Now().UTC()}}}, + } + _, err := suite.msgServer.UpdateState(suite.Ctx, &updateState) + suite.NoError(err) + + // update state without timestamp + updateState = types.MsgUpdateState{ + Creator: proposer, + RollappId: rollappId, + StartHeight: 3, + NumBlocks: 1, + DAPath: "", + BDs: types.BlockDescriptors{BD: []types.BlockDescriptor{{Height: 3}}}, + } + _, err = suite.msgServer.UpdateState(suite.Ctx, &updateState) + suite.ErrorIs(err, types.ErrInvalidBlockDescriptorTimestamp) +} + // --------------------------------------- // verifyAll receives a list of expected results and a map of rollapId->rollapp // the function verifies that the map contains all the rollapps that are in the list and only them diff --git a/x/rollapp/transfergenesis/ante_decorator.go b/x/rollapp/transfergenesis/ante_decorator.go index 543a9911b..963fe3789 100644 --- a/x/rollapp/transfergenesis/ante_decorator.go +++ b/x/rollapp/transfergenesis/ante_decorator.go @@ -7,11 +7,16 @@ import ( "github.com/dymensionxyz/sdk-utils/utils/uibc" transferTypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" "github.com/dymensionxyz/dymension/v3/x/rollapp/types" ) type GetRollapp func(ctx sdk.Context, rollappId string) (val types.Rollapp, found bool) +type ChannelKeeper interface { + GetChannelClientState(ctx sdk.Context, portID, channelID string) (string, exported.ClientState, error) // implemented by ibc channel keeper +} + // TransferEnabledDecorator only allows ibc transfers to a rollapp if that rollapp has finished // the transfer genesis protocol. type TransferEnabledDecorator struct { diff --git a/x/rollapp/transfergenesis/ibc_module_canonical_channel_hack.go b/x/rollapp/transfergenesis/ibc_module_canonical_channel_hack.go deleted file mode 100644 index d0ade93c2..000000000 --- a/x/rollapp/transfergenesis/ibc_module_canonical_channel_hack.go +++ /dev/null @@ -1,56 +0,0 @@ -package transfergenesis - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" - porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" - "github.com/cosmos/ibc-go/v7/modules/core/exported" - rollappkeeper "github.com/dymensionxyz/dymension/v3/x/rollapp/keeper" - uibc "github.com/dymensionxyz/sdk-utils/utils/uibc" -) - -/* -TODO: this whole file is temporary - Prior to this we relied on the whitelist addr to set the canonical channel, but that is no longer possible - This currently file is a hack (not secure) - The real solution will come in a followup PR - See https://github.com/dymensionxyz/research/issues/242 -*/ - -type ChannelKeeper interface { - GetChannelClientState(ctx sdk.Context, portID, channelID string) (string, exported.ClientState, error) // implemented by ibc channel keeper -} - -type IBCModuleCanonicalChannelHack struct { - porttypes.IBCModule // next one - rollappKeeper rollappkeeper.Keeper - channelKeeper ChannelKeeper -} - -func NewIBCModuleCanonicalChannelHack( - next porttypes.IBCModule, - rollappKeeper rollappkeeper.Keeper, - channelKeeper ChannelKeeper, -) *IBCModuleCanonicalChannelHack { - return &IBCModuleCanonicalChannelHack{IBCModule: next, rollappKeeper: rollappKeeper, channelKeeper: channelKeeper} -} - -func (w IBCModuleCanonicalChannelHack) OnRecvPacket( - ctx sdk.Context, - packet channeltypes.Packet, - relayer sdk.AccAddress, -) exported.Acknowledgement { - l := ctx.Logger().With("module", "hack set canonical channel") - - chainID, err := uibc.ChainIDFromPortChannel(ctx, w.channelKeeper, packet.GetDestPort(), packet.GetDestChannel()) - if err != nil { - return channeltypes.NewErrorAcknowledgement(err) - } - ra, ok := w.rollappKeeper.GetRollapp(ctx, chainID) - if ok && ra.ChannelId == "" { - ra.ChannelId = packet.GetDestChannel() - w.rollappKeeper.SetRollapp(ctx, ra) - l.Info("Set the canonical channel.", "channel id", packet.GetDestChannel()) - } - return w.IBCModule.OnRecvPacket(ctx, packet, relayer) -} diff --git a/x/rollapp/types/block_descriptor.go b/x/rollapp/types/block_descriptor.go new file mode 100644 index 000000000..a9c19ec65 --- /dev/null +++ b/x/rollapp/types/block_descriptor.go @@ -0,0 +1,19 @@ +package types + +import errorsmod "cosmossdk.io/errors" + +func (bds BlockDescriptors) Validate() error { + for _, bd := range bds.BD { + if err := bd.Validate(); err != nil { + return err + } + } + return nil +} + +func (bd BlockDescriptor) Validate() error { + if bd.Timestamp.IsZero() { + return errorsmod.Wrapf(ErrInvalidBlockDescriptorTimestamp, "timestamp is empty for block descriptor at height %d", bd.Height) + } + return nil +} diff --git a/x/rollapp/types/block_descriptor.pb.go b/x/rollapp/types/block_descriptor.pb.go index 5817dcf5c..746dc5b83 100644 --- a/x/rollapp/types/block_descriptor.pb.go +++ b/x/rollapp/types/block_descriptor.pb.go @@ -7,15 +7,19 @@ import ( fmt "fmt" _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" + _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -29,6 +33,8 @@ type BlockDescriptor struct { Height uint64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` // stateRoot is a 32 byte array of the hash of the block (state root of the block) StateRoot []byte `protobuf:"bytes,2,opt,name=stateRoot,proto3" json:"stateRoot,omitempty"` + // timestamp is the time from the block header + Timestamp time.Time `protobuf:"bytes,3,opt,name=timestamp,proto3,stdtime" json:"timestamp"` } func (m *BlockDescriptor) Reset() { *m = BlockDescriptor{} } @@ -78,6 +84,13 @@ func (m *BlockDescriptor) GetStateRoot() []byte { return nil } +func (m *BlockDescriptor) GetTimestamp() time.Time { + if m != nil { + return m.Timestamp + } + return time.Time{} +} + // BlockDescriptors defines list of BlockDescriptor. type BlockDescriptors struct { BD []BlockDescriptor `protobuf:"bytes,1,rep,name=BD,proto3" json:"BD"` @@ -133,23 +146,26 @@ func init() { } var fileDescriptor_6eb4c1d0c21c2e68 = []byte{ - // 243 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x4d, 0xa9, 0xcc, 0x4d, - 0xcd, 0x2b, 0xce, 0xcc, 0xcf, 0xab, 0xa8, 0xac, 0xd2, 0x87, 0x73, 0xf4, 0x8b, 0xf2, 0x73, 0x72, - 0x12, 0x0b, 0x0a, 0xf4, 0x93, 0x72, 0xf2, 0x93, 0xb3, 0xe3, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, - 0x0b, 0x4a, 0xf2, 0x8b, 0xf4, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0xe4, 0x90, 0xb5, 0xe9, 0xc1, - 0x39, 0x7a, 0x50, 0x6d, 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0xa5, 0xfa, 0x20, 0x16, 0x44, - 0x97, 0x92, 0x3b, 0x17, 0xbf, 0x13, 0xc8, 0x3c, 0x17, 0xb8, 0x71, 0x42, 0x62, 0x5c, 0x6c, 0x19, - 0xa9, 0x99, 0xe9, 0x19, 0x25, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x2c, 0x41, 0x50, 0x9e, 0x90, 0x0c, - 0x17, 0x67, 0x71, 0x49, 0x62, 0x49, 0x6a, 0x50, 0x7e, 0x7e, 0x89, 0x04, 0x93, 0x02, 0xa3, 0x06, - 0x4f, 0x10, 0x42, 0x40, 0x29, 0x92, 0x4b, 0x00, 0xcd, 0xa0, 0x62, 0x21, 0x57, 0x2e, 0x26, 0x27, - 0x17, 0x09, 0x46, 0x05, 0x66, 0x0d, 0x6e, 0x23, 0x7d, 0x3d, 0xfc, 0xee, 0xd3, 0x43, 0xd3, 0xed, - 0xc4, 0x72, 0xe2, 0x9e, 0x3c, 0x43, 0x10, 0x93, 0x93, 0x8b, 0x93, 0xdf, 0x89, 0x47, 0x72, 0x8c, - 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, - 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x99, 0xa4, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, - 0xe7, 0xea, 0xe3, 0x08, 0xb5, 0x32, 0x63, 0xfd, 0x0a, 0x78, 0xd0, 0x95, 0x54, 0x16, 0xa4, 0x16, - 0x27, 0xb1, 0x81, 0xbd, 0x6e, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x40, 0x09, 0x0c, 0xda, 0x69, - 0x01, 0x00, 0x00, + // 303 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x91, 0xc1, 0x4e, 0xc2, 0x30, + 0x18, 0xc7, 0x57, 0x20, 0x44, 0x8a, 0x89, 0x66, 0x31, 0x66, 0x21, 0xa6, 0x2c, 0x9c, 0x76, 0x6a, + 0x13, 0xd0, 0x17, 0x68, 0xf0, 0xea, 0x61, 0xf1, 0xa2, 0x17, 0xc3, 0xa0, 0x96, 0xc5, 0x8d, 0xaf, + 0x59, 0x8b, 0x61, 0xbe, 0x82, 0x17, 0x1e, 0x8b, 0x23, 0x47, 0x4f, 0x6a, 0xb6, 0x17, 0x31, 0xdb, + 0x60, 0x18, 0x12, 0xbd, 0xf5, 0xdf, 0xfc, 0x7f, 0xfd, 0xb5, 0xfd, 0xf0, 0xcd, 0x2c, 0x8d, 0xc5, + 0x42, 0x87, 0xb0, 0x58, 0xa5, 0x6f, 0xac, 0x0e, 0x2c, 0x81, 0x28, 0x9a, 0x28, 0xc5, 0x82, 0x08, + 0xa6, 0x2f, 0x4f, 0x33, 0xa1, 0xa7, 0x49, 0xa8, 0x0c, 0x24, 0x54, 0x25, 0x60, 0xc0, 0x26, 0xbf, + 0x31, 0x5a, 0x07, 0xba, 0xc3, 0x7a, 0x17, 0x12, 0x24, 0x94, 0x55, 0x56, 0xac, 0x2a, 0xaa, 0xd7, + 0x97, 0x00, 0x32, 0x12, 0xac, 0x4c, 0xc1, 0xf2, 0x99, 0x99, 0x30, 0x16, 0xda, 0x4c, 0x62, 0x55, + 0x15, 0x06, 0xef, 0x08, 0x9f, 0xf1, 0xc2, 0x38, 0xae, 0x85, 0xf6, 0x25, 0x6e, 0xcf, 0x45, 0x28, + 0xe7, 0xc6, 0x41, 0x2e, 0xf2, 0x5a, 0xfe, 0x2e, 0xd9, 0x57, 0xb8, 0xa3, 0xcd, 0xc4, 0x08, 0x1f, + 0xc0, 0x38, 0x0d, 0x17, 0x79, 0xa7, 0xfe, 0x61, 0xc3, 0xe6, 0xb8, 0x53, 0x1f, 0xee, 0x34, 0x5d, + 0xe4, 0x75, 0x87, 0x3d, 0x5a, 0xe9, 0xe9, 0x5e, 0x4f, 0xef, 0xf7, 0x0d, 0x7e, 0xb2, 0xf9, 0xec, + 0x5b, 0xeb, 0xaf, 0x3e, 0xf2, 0x0f, 0xd8, 0xe0, 0x01, 0x9f, 0x1f, 0x5d, 0x46, 0xdb, 0xb7, 0xb8, + 0xc1, 0xc7, 0x0e, 0x72, 0x9b, 0x5e, 0x77, 0xc8, 0xe8, 0xff, 0xbf, 0x40, 0x8f, 0x68, 0xde, 0x2a, + 0x2c, 0x7e, 0x83, 0x8f, 0xf9, 0xdd, 0x26, 0x23, 0x68, 0x9b, 0x11, 0xf4, 0x9d, 0x11, 0xb4, 0xce, + 0x89, 0xb5, 0xcd, 0x89, 0xf5, 0x91, 0x13, 0xeb, 0xf1, 0x5a, 0x86, 0x66, 0xbe, 0x0c, 0xe8, 0x14, + 0x62, 0xf6, 0xc7, 0x6c, 0x5e, 0x47, 0x6c, 0x55, 0x0f, 0xc8, 0xa4, 0x4a, 0xe8, 0xa0, 0x5d, 0xbe, + 0x69, 0xf4, 0x13, 0x00, 0x00, 0xff, 0xff, 0x4b, 0x62, 0xb7, 0x2c, 0xcf, 0x01, 0x00, 0x00, } func (m *BlockDescriptor) Marshal() (dAtA []byte, err error) { @@ -172,6 +188,14 @@ func (m *BlockDescriptor) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + n1, err1 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(m.Timestamp, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(m.Timestamp):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintBlockDescriptor(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x1a if len(m.StateRoot) > 0 { i -= len(m.StateRoot) copy(dAtA[i:], m.StateRoot) @@ -248,6 +272,8 @@ func (m *BlockDescriptor) Size() (n int) { if l > 0 { n += 1 + l + sovBlockDescriptor(uint64(l)) } + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(m.Timestamp) + n += 1 + l + sovBlockDescriptor(uint64(l)) return n } @@ -354,6 +380,39 @@ func (m *BlockDescriptor) Unmarshal(dAtA []byte) error { m.StateRoot = []byte{} } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBlockDescriptor + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBlockDescriptor + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBlockDescriptor + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipBlockDescriptor(dAtA[iNdEx:]) diff --git a/x/rollapp/types/block_descriptor_test.go b/x/rollapp/types/block_descriptor_test.go new file mode 100644 index 000000000..a8184cff0 --- /dev/null +++ b/x/rollapp/types/block_descriptor_test.go @@ -0,0 +1,97 @@ +package types_test + +import ( + "testing" + time "time" + + "github.com/dymensionxyz/dymension/v3/x/rollapp/types" + "github.com/stretchr/testify/require" +) + +func TestBlockDescriptorsValidate(t *testing.T) { + testCases := []struct { + name string + bds types.BlockDescriptors + expPass bool + }{ + { + name: "valid block descriptors", + bds: types.BlockDescriptors{ + BD: []types.BlockDescriptor{ + { + Height: 1, + Timestamp: time.Now(), + }, + { + Height: 2, + Timestamp: time.Now(), + }, + }, + }, + expPass: true, + }, + { + name: "invalid block descriptor", + bds: types.BlockDescriptors{ + BD: []types.BlockDescriptor{ + { + Height: 1, + Timestamp: time.Now(), + }, + { + Height: 2, + }, + }, + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := tc.bds.Validate() + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +func TestBlockDescriptorValidate(t *testing.T) { + testCases := []struct { + name string + bd types.BlockDescriptor + expPass bool + }{ + { + name: "valid block descriptor", + bd: types.BlockDescriptor{ + Height: 1, + Timestamp: time.Now(), + }, + expPass: true, + }, + { + name: "invalid block descriptor", + bd: types.BlockDescriptor{ + Height: 1, + }, + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := tc.bd.Validate() + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/x/rollapp/types/errors.go b/x/rollapp/types/errors.go index ef26ee887..2805b79cc 100644 --- a/x/rollapp/types/errors.go +++ b/x/rollapp/types/errors.go @@ -40,6 +40,7 @@ var ( ErrSameOwner = errorsmod.Wrap(gerrc.ErrInvalidArgument, "same owner") ErrInvalidRequest = errorsmod.Wrap(gerrc.ErrInvalidArgument, "invalid request") ErrInvalidVMType = errorsmod.Wrap(gerrc.ErrInvalidArgument, "invalid vm type") + ErrInvalidBlockDescriptorTimestamp = errorsmod.Wrap(gerrc.ErrInvalidArgument, "invalid block descriptor timestamp") /* ------------------------------ fraud related ----------------------------- */ ErrDisputeAlreadyFinalized = errorsmod.Register(ModuleName, 2000, "disputed height already finalized") diff --git a/x/rollapp/types/hooks.go b/x/rollapp/types/hooks.go index 4a1671297..937335649 100644 --- a/x/rollapp/types/hooks.go +++ b/x/rollapp/types/hooks.go @@ -12,6 +12,7 @@ import ( // RollappHooks event hooks for rollapp object (noalias) type RollappHooks interface { BeforeUpdateState(ctx sdk.Context, seqAddr, rollappId string, lastStateUpdateBySequencer bool) error // Must be called when a rollapp's state changes + AfterUpdateState(ctx sdk.Context, rollappID string, stateInfo *StateInfo) error // Must be called when a rollapp's state changes AfterStateFinalized(ctx sdk.Context, rollappID string, stateInfo *StateInfo) error // Must be called when a rollapp's state changes FraudSubmitted(ctx sdk.Context, rollappID string, height uint64, seqAddr string) error RollappCreated(ctx sdk.Context, rollappID, alias string, creator sdk.AccAddress) error @@ -37,6 +38,16 @@ func (h MultiRollappHooks) BeforeUpdateState(ctx sdk.Context, seqAddr, rollappId return nil } +func (h MultiRollappHooks) AfterUpdateState(ctx sdk.Context, rollappID string, stateInfo *StateInfo) error { + for i := range h { + err := h[i].AfterUpdateState(ctx, rollappID, stateInfo) + if err != nil { + return err + } + } + return nil +} + func (h MultiRollappHooks) AfterStateFinalized(ctx sdk.Context, rollappID string, stateInfo *StateInfo) error { for i := range h { err := h[i].AfterStateFinalized(ctx, rollappID, stateInfo) @@ -75,8 +86,12 @@ type StubRollappCreatedHooks struct{} func (StubRollappCreatedHooks) RollappCreated(sdk.Context, string, string, sdk.AccAddress) error { return nil } + func (StubRollappCreatedHooks) BeforeUpdateState(sdk.Context, string, string, bool) error { return nil } -func (StubRollappCreatedHooks) FraudSubmitted(sdk.Context, string, uint64, string) error { return nil } +func (StubRollappCreatedHooks) AfterUpdateState(sdk.Context, string, *StateInfo) error { + return nil +} +func (StubRollappCreatedHooks) FraudSubmitted(sdk.Context, string, uint64, string) error { return nil } func (StubRollappCreatedHooks) AfterStateFinalized(sdk.Context, string, *StateInfo) error { return nil } diff --git a/x/rollapp/types/state_info.go b/x/rollapp/types/state_info.go index c8e160700..a90bf3cce 100644 --- a/x/rollapp/types/state_info.go +++ b/x/rollapp/types/state_info.go @@ -35,6 +35,22 @@ func (s *StateInfo) GetLatestHeight() uint64 { return s.StartHeight + s.NumBlocks - 1 } +func (s *StateInfo) ContainsHeight(height uint64) bool { + return s.StartHeight <= height && height <= s.GetLatestHeight() +} + +func (s *StateInfo) GetBlockDescriptor(height uint64) (BlockDescriptor, bool) { + if !s.ContainsHeight(height) { + return BlockDescriptor{}, false + } + return s.BDs.BD[height-s.StartHeight], true +} + +func (s *StateInfo) GetLatestBlockDescriptor() BlockDescriptor { + // return s.BDs.BD[s.NumBlocks-1] // todo: should it be this? or the one below? using this breaks ibctesting tests + return s.BDs.BD[len(s.BDs.BD)-1] +} + func (s *StateInfo) GetEvents() []sdk.Attribute { eventAttributes := []sdk.Attribute{ sdk.NewAttribute(AttributeKeyRollappId, s.StateInfoIndex.RollappId), diff --git a/x/sequencer/types/sequencer.go b/x/sequencer/types/sequencer.go index e150c32d2..9e6edffbf 100644 --- a/x/sequencer/types/sequencer.go +++ b/x/sequencer/types/sequencer.go @@ -1,6 +1,7 @@ package types import ( + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" cometbfttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -38,13 +39,7 @@ func (seq Sequencer) IsNoticePeriodInProgress() bool { // GetDymintPubKeyHash returns the hash of the sequencer // as expected to be written on the rollapp ibc client headers func (seq Sequencer) GetDymintPubKeyHash() ([]byte, error) { - // load the dymint pubkey into a cryptotypes.PubKey - interfaceRegistry := cdctypes.NewInterfaceRegistry() - cryptocodec.RegisterInterfaces(interfaceRegistry) - protoCodec := codec.NewProtoCodec(interfaceRegistry) - - var pubKey cryptotypes.PubKey - err := protoCodec.UnpackAny(seq.DymintPubKey, &pubKey) + pubKey, err := seq.getCosmosPubKey() if err != nil { return nil, err } @@ -61,3 +56,25 @@ func (seq Sequencer) GetDymintPubKeyHash() ([]byte, error) { tmValidatorSet := cometbfttypes.NewValidatorSet([]*cometbfttypes.Validator{tmValidator}) return tmValidatorSet.Hash(), nil } + +// GetCometPubKey returns the bytes of the sequencer's dymint pubkey +func (seq Sequencer) GetCometPubKey() (tmprotocrypto.PublicKey, error) { + pubKey, err := seq.getCosmosPubKey() + if err != nil { + return tmprotocrypto.PublicKey{}, err + } + + // convert the pubkey to tmPubKey + tmPubKey, err := cryptocodec.ToTmProtoPublicKey(pubKey) + return tmPubKey, err +} + +func (seq Sequencer) getCosmosPubKey() (cryptotypes.PubKey, error) { + interfaceRegistry := cdctypes.NewInterfaceRegistry() + cryptocodec.RegisterInterfaces(interfaceRegistry) + protoCodec := codec.NewProtoCodec(interfaceRegistry) + + var pubKey cryptotypes.PubKey + err := protoCodec.UnpackAny(seq.DymintPubKey, &pubKey) + return pubKey, err +}