Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add UpgradeChain and UpgradeClient endpoint test functions #1169

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo, update changelog entry


### Features

### Bug Fixes
Expand Down
18 changes: 15 additions & 3 deletions testing/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 4 additions & 2 deletions testing/chain_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -36,3 +37,4 @@ func TestChangeValSet(t *testing.T) {
path.EndpointB.UpdateClient()
path.EndpointB.UpdateClient()
}
*/
208 changes: 208 additions & 0 deletions testing/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions testing/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -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)
}