diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b655f7c6d4..4b0b612a4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +* (testing) [\#1169](https://github.com/cosmos/ibc-go/pull/1169) Add `UpgradeChain` and `UpgradeClient` helper functions to the testing `Endpoint` structure. + ### Features ### Bug Fixes diff --git a/testing/chain.go b/testing/chain.go index 9e248312bc8..693df867071 100644 --- a/testing/chain.go +++ b/testing/chain.go @@ -184,6 +184,11 @@ func (chain *TestChain) GetContext() sdk.Context { return chain.App.GetBaseApp().NewContext(false, chain.CurrentHeader) } +// GetContext returns the current context for the application. +func (chain *TestChain) GetCheckTxContext() sdk.Context { + return chain.App.GetBaseApp().NewContext(true, chain.CurrentHeader) +} + // GetSimApp returns the SimApp to allow usage ofnon-interface fields. // CONTRACT: This function should not be called by third parties implementing // their own SimApp. @@ -200,11 +205,18 @@ func (chain *TestChain) QueryProof(key []byte) ([]byte, clienttypes.Height) { return chain.QueryProofAtHeight(key, chain.App.LastBlockHeight()) } +// QueryProofAtHeight performs an abci query with the given key and returns the proto encoded merkle proof +// for the query and the height at which the proof will succeed on a tendermint verifier. Only the IBC +// store is supported +func (chain *TestChain) QueryProofAtHeight(key []byte, height int64) ([]byte, clienttypes.Height) { + return chain.QueryProofForStore(host.StoreKey, key, chain.App.LastBlockHeight()) +} + // QueryProof performs an abci query with the given key and returns the proto encoded merkle proof // for the query and the height at which the proof will succeed on a tendermint verifier. -func (chain *TestChain) QueryProofAtHeight(key []byte, height int64) ([]byte, clienttypes.Height) { +func (chain *TestChain) QueryProofForStore(storeKey string, key []byte, height int64) ([]byte, clienttypes.Height) { res := chain.App.Query(abci.RequestQuery{ - Path: fmt.Sprintf("store/%s/key", host.StoreKey), + Path: fmt.Sprintf("store/%s/key", storeKey), Height: height - 1, Data: key, Prove: true, @@ -352,7 +364,7 @@ func (chain *TestChain) GetConsensusState(clientID string, height exported.Heigh // GetValsAtHeight will return the validator set of the chain at a given height. It will return // a success boolean depending on if the validator set exists or not at that height. func (chain *TestChain) GetValsAtHeight(height int64) (*tmtypes.ValidatorSet, bool) { - histInfo, ok := chain.App.GetStakingKeeper().GetHistoricalInfo(chain.GetContext(), height) + histInfo, ok := chain.App.GetStakingKeeper().GetHistoricalInfo(chain.GetCheckTxContext(), height) if !ok { return nil, false } diff --git a/testing/chain_test.go b/testing/chain_test.go index 64ddc6c751e..0becbe7312c 100644 --- a/testing/chain_test.go +++ b/testing/chain_test.go @@ -1,12 +1,13 @@ package ibctesting_test +/* import ( "testing" - "github.com/stretchr/testify/require" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + ibctesting "github.com/cosmos/ibc-go/v3/testing" ) @@ -36,3 +37,4 @@ func TestChangeValSet(t *testing.T) { path.EndpointB.UpdateClient() path.EndpointB.UpdateClient() } +*/ diff --git a/testing/endpoint.go b/testing/endpoint.go index 607f7a16843..9b7d7d81550 100644 --- a/testing/endpoint.go +++ b/testing/endpoint.go @@ -4,7 +4,10 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + // "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/stretchr/testify/require" + // abci "github.com/tendermint/tendermint/abci/types" clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" @@ -13,6 +16,7 @@ import ( host "github.com/cosmos/ibc-go/v3/modules/core/24-host" "github.com/cosmos/ibc-go/v3/modules/core/exported" ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + "github.com/cosmos/ibc-go/v3/testing/simapp" ) // Endpoint is a which represents a channel endpoint and its associated @@ -492,6 +496,210 @@ func (endpoint *Endpoint) TimeoutOnClose(packet channeltypes.Packet) error { return endpoint.Chain.sendMsgs(timeoutOnCloseMsg) } +// UpgradeChain performs an IBC client upgrade using the provided client state. +// The chainID within the client state will have its revision number incremented by 1. +// The counterparty client will be upgraded if it exists. +// The upgrade is performed at the current height + 2. The upgradeClientState is set +// at the current height, and the upgradeConsensusState is set at current height + 1. +// At the upgrade height, the upgrade module will produce a panic to perform the upgrade. +// This panic is caught, the counterparty client is upgraded and the chainID is switched. +func (endpoint *Endpoint) UpgradeChain(clientState *ibctmtypes.ClientState) (err error) { + if endpoint.Counterparty.ClientID == "" { + return fmt.Errorf("cannot upgrade chain if there is no counterparty client") + } + + // the upgrade will be perfromed in 2 blocks + // the current block will commit the upgradeClientState into state + // the next block will commit the upgradeConsensusState into state via begin blocker + // the upgrade height will be used to update the counterparty client and perform the upgrade + upgradeHeight := endpoint.Chain.GetContext().BlockHeight() + 2 + + // increment revision number in chainID + oldChainID := clientState.ChainId + revisionNumber := clienttypes.ParseChainID(oldChainID) + newChainID, err := clienttypes.SetRevisionNumber(oldChainID, revisionNumber+1) + if err != nil { + // current chainID is not in revision format + newChainID = clientState.ChainId + "-1" + } + + clientState.ChainId = newChainID + clientState.LatestHeight = clienttypes.NewHeight(revisionNumber+1, clientState.LatestHeight.GetRevisionHeight()+1) + upgradeName := fmt.Sprintf("upgrade chain %s to %s", oldChainID, newChainID) + + upgradePlan := upgradetypes.Plan{ + Name: upgradeName, + Height: upgradeHeight, + } + + // construct upgrade proposal + upgradeProposal, err := clienttypes.NewUpgradeProposal(upgradeName, "the testing chain is being upgraded to a new chainID with an incermented revision", upgradePlan, clientState) + if err != nil { + return err + } + + // schedule upgrade + if err := endpoint.Chain.GetSimApp().IBCKeeper.ClientKeeper.HandleUpgradeProposal(endpoint.Chain.GetContext(), upgradeProposal.(*clienttypes.UpgradeProposal)); err != nil { + return err + } + + // commit current block with client state set in state + // begin block will be called on upgradeHeight - 1 + // which will cause the upgrade consensus state to be set + endpoint.Chain.NextBlock() + + // handle the upgrade + // when the upgrade height is reached, a panic is executed which will be caught by this defer function + // the counterparty client will be upgraded, the upgrade will perform a no-op migration + // and the chainID will be switched to the newChainID + // TODO: handle begin block panic + /* + defer func() { + if r := recover(); r != nil { + fmt.Println("Upgrading") + // if err = endpoint.Counterparty.upgradeClient(upgradeHeight); err != nil { + // return + // } + + fmt.Println("handler set") + endpoint.Chain.GetSimApp().UpgradeKeeper.SetUpgradeHandler( + upgradeName, + func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // no-op upgrade handler + return fromVM, nil + }, + ) + + // update our chainID so future commits use the correct chainID + endpoint.Chain.ChainID = newChainID + endpoint.Chain.CurrentHeader.ChainID = newChainID + } + }() + */ + + // perform upgrade + // the upgrade consensus state will be committed + // begin block will be called for the upgradeHeight causing a panic + // the panic will be caught by the above defer function + // Mock TM process, commit last app state and allow next header to be generated + // before calling 'BeginBlock' + endpoint.Chain.App.Commit() + endpoint.Chain.CurrentHeader.Height = endpoint.Chain.CurrentHeader.Height + 1 + endpoint.Chain.CurrentHeader.AppHash = endpoint.Chain.App.LastCommitID().Hash + + if err = endpoint.Counterparty.upgradeClient(upgradeHeight); err != nil { + return err + } + + return nil +} + +func (endpoint *Endpoint) upgradeClient(upgradeHeight int64) error { + var msg sdk.Msg + + // update client to latest state which contains the upgrade client and consensus states + // header, err := endpoint.Chain.ConstructUpdateTMClientHeader(endpoint.Counterparty.Chain, endpoint.ClientID) + trustedHeight := endpoint.GetClientState().GetLatestHeight().(clienttypes.Height) + + trustedVals, found := endpoint.Counterparty.Chain.GetValsAtHeight(int64(trustedHeight.RevisionHeight) + 1) + require.True(endpoint.Chain.T, found) + + // passing the CurrentHeader.Height as the block height as it will become a previous height once we commit N blocks + header := endpoint.Counterparty.Chain.CreateTMClientHeader(endpoint.Counterparty.Chain.ChainID, endpoint.Counterparty.Chain.CurrentHeader.Height, trustedHeight, endpoint.Counterparty.Chain.CurrentHeader.Time, endpoint.Counterparty.Chain.Vals, endpoint.Counterparty.Chain.NextVals, trustedVals, endpoint.Counterparty.Chain.Signers) + msg, err := clienttypes.NewMsgUpdateClient( + endpoint.ClientID, header, + endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.T, err) + + // TODO: + // The functionality of 'SendMsgs` is copied and pasted + // except for the call to coord.IncrementTime() + // which causes begin block to be called on the counterparty (resulting in a panic) + // + // err = endpoint.Chain.sendMsgs(msg) + // require.NoError(endpoint.Chain.T, err) + // ensure the chain has the latest time + endpoint.Chain.Coordinator.UpdateTimeForChain(endpoint.Chain) + + _, _, err = simapp.SignAndDeliver( + endpoint.Chain.T, + endpoint.Chain.TxConfig, + endpoint.Chain.App.GetBaseApp(), + endpoint.Chain.GetContext().BlockHeader(), + []sdk.Msg{msg}, + endpoint.Chain.ChainID, + []uint64{endpoint.Chain.SenderAccount.GetAccountNumber()}, + []uint64{endpoint.Chain.SenderAccount.GetSequence()}, + true, true, endpoint.Chain.SenderPrivKey, + ) + if err != nil { + return err + } + + // NextBlock calls app.Commit() + endpoint.Chain.NextBlock() + + // increment sequence for successful transaction execution + endpoint.Chain.SenderAccount.SetSequence(endpoint.Chain.SenderAccount.GetSequence() + 1) + + // prepare upgrade client message + + clientStateBz, found := endpoint.Counterparty.Chain.GetSimApp().IBCKeeper.ClientKeeper.GetUpgradedClient(endpoint.Counterparty.Chain.GetCheckTxContext(), upgradeHeight) + require.True(endpoint.Chain.T, found) + clientState := clienttypes.MustUnmarshalClientState(endpoint.Counterparty.Chain.App.AppCodec(), clientStateBz) + + consensusStateBz, found := endpoint.Counterparty.Chain.GetSimApp().IBCKeeper.ClientKeeper.GetUpgradedConsensusState(endpoint.Counterparty.Chain.GetCheckTxContext(), upgradeHeight) + require.True(endpoint.Chain.T, found) + consensusState := clienttypes.MustUnmarshalConsensusState(endpoint.Counterparty.Chain.App.AppCodec(), consensusStateBz) + + // the lastHeight should be used for generating proofs + lastHeight := int64(endpoint.GetClientState().GetLatestHeight().GetRevisionHeight()) + + clientKey := upgradetypes.UpgradedClientKey(upgradeHeight) + proofUpgradeClient, _ := endpoint.Counterparty.Chain.QueryProofForStore(upgradetypes.StoreKey, clientKey, lastHeight) + + consensusKey := upgradetypes.UpgradedConsStateKey(upgradeHeight) + proofUpgradeConsState, _ := endpoint.Counterparty.Chain.QueryProofForStore(upgradetypes.StoreKey, consensusKey, lastHeight) + + // upgrade counterparty client + msg, err = clienttypes.NewMsgUpgradeClient( + endpoint.ClientID, clientState, consensusState, proofUpgradeClient, proofUpgradeConsState, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.T, err) + + // TODO: + // if _, err = endpoint.Chain.SendMsgs(msg); err != nil { + // return err + // } + endpoint.Chain.Coordinator.UpdateTimeForChain(endpoint.Chain) + + _, _, err = simapp.SignAndDeliver( + endpoint.Chain.T, + endpoint.Chain.TxConfig, + endpoint.Chain.App.GetBaseApp(), + endpoint.Chain.GetContext().BlockHeader(), + []sdk.Msg{msg}, + endpoint.Chain.ChainID, + []uint64{endpoint.Chain.SenderAccount.GetAccountNumber()}, + []uint64{endpoint.Chain.SenderAccount.GetSequence()}, + true, true, endpoint.Chain.SenderPrivKey, + ) + if err != nil { + return err + } + + // NextBlock calls app.Commit() + endpoint.Chain.NextBlock() + + // increment sequence for successful transaction execution + endpoint.Chain.SenderAccount.SetSequence(endpoint.Chain.SenderAccount.GetSequence() + 1) + + fmt.Println("upgrade complete") + + return nil +} + // SetChannelClosed sets a channel state to CLOSED. func (endpoint *Endpoint) SetChannelClosed() error { channel := endpoint.GetChannel() diff --git a/testing/endpoint_test.go b/testing/endpoint_test.go new file mode 100644 index 00000000000..1aad60c3761 --- /dev/null +++ b/testing/endpoint_test.go @@ -0,0 +1,23 @@ +package ibctesting_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + ibctesting "github.com/cosmos/ibc-go/v3/testing" +) + +func TestUpgradeChain(t *testing.T) { + coord := ibctesting.NewCoordinator(t, 2) + chainA := coord.GetChain(ibctesting.GetChainID(1)) + chainB := coord.GetChain(ibctesting.GetChainID(2)) + + path := ibctesting.NewPath(chainA, chainB) + err := path.EndpointA.CreateClient() + require.NoError(t, err) + + err = path.EndpointB.UpgradeChain(path.EndpointA.GetClientState().(*ibctmtypes.ClientState)) + require.NoError(t, err) +}