cosmos-sdk/x/ibc/testing/coordinator.go

530 lines
15 KiB
Go

package testing
import (
"fmt"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
clientexported "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported"
channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types"
)
var (
ChainIDPrefix = "testchain"
globalStartTime = time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)
timeIncrement = time.Second * 5
)
// Coordinator is a testing struct which contains N TestChain's. It handles keeping all chains
// in sync with regards to time.
type Coordinator struct {
t *testing.T
Chains map[string]*TestChain
}
// NewCoordinator initializes Coordinator with N TestChain's
func NewCoordinator(t *testing.T, n int) *Coordinator {
chains := make(map[string]*TestChain)
for i := 0; i < n; i++ {
chainID := GetChainID(i)
chains[chainID] = NewTestChain(t, chainID)
}
return &Coordinator{
t: t,
Chains: chains,
}
}
// Setup constructs a TM client, connection, and channel on both chains provided. It will
// fails if any error occurs. The clientID's, TestConnections, and TestChannels are returned
// for both chains.
func (coord *Coordinator) Setup(
chainA, chainB *TestChain,
) (string, string, *TestConnection, *TestConnection, TestChannel, TestChannel) {
clientA, clientB, connA, connB := coord.SetupClientConnections(chainA, chainB, clientexported.Tendermint)
// channels can also be referenced through the returned connections
channelA, channelB := coord.CreateChannel(chainA, chainB, connA, connB, channeltypes.UNORDERED)
return clientA, clientB, connA, connB, channelA, channelB
}
// SetupClients is a helper function to create clients on both chains. It assumes the
// caller does not anticipate any errors.
func (coord *Coordinator) SetupClients(
chainA, chainB *TestChain,
clientType clientexported.ClientType,
) (string, string) {
clientA, err := coord.CreateClient(chainA, chainB, clientType)
require.NoError(coord.t, err)
clientB, err := coord.CreateClient(chainB, chainA, clientType)
require.NoError(coord.t, err)
return clientA, clientB
}
// SetupClientConnections is a helper function to create clients and the appropriate
// connections on both the source and counterparty chain. It assumes the caller does not
// anticipate any errors.
func (coord *Coordinator) SetupClientConnections(
chainA, chainB *TestChain,
clientType clientexported.ClientType,
) (string, string, *TestConnection, *TestConnection) {
clientA, clientB := coord.SetupClients(chainA, chainB, clientType)
connA, connB := coord.CreateConnection(chainA, chainB, clientA, clientB)
return clientA, clientB, connA, connB
}
// CreateClient creates a counterparty client on the source chain and returns the clientID.
func (coord *Coordinator) CreateClient(
source, counterparty *TestChain,
clientType clientexported.ClientType,
) (clientID string, err error) {
coord.CommitBlock(source, counterparty)
clientID = source.NewClientID(counterparty.ChainID)
switch clientType {
case clientexported.Tendermint:
err = source.CreateTMClient(counterparty, clientID)
default:
err = fmt.Errorf("client type %s is not supported", clientType)
}
if err != nil {
return "", err
}
coord.IncrementTime()
return clientID, nil
}
// UpdateClient updates a counterparty client on the source chain.
func (coord *Coordinator) UpdateClient(
source, counterparty *TestChain,
clientID string,
clientType clientexported.ClientType,
) (err error) {
coord.CommitBlock(source, counterparty)
switch clientType {
case clientexported.Tendermint:
err = source.UpdateTMClient(counterparty, clientID)
default:
err = fmt.Errorf("client type %s is not supported", clientType)
}
if err != nil {
return err
}
coord.IncrementTime()
return nil
}
// CreateConnection constructs and executes connection handshake messages in order to create
// OPEN channels on chainA and chainB. The connection information of for chainA and chainB
// are returned within a TestConnection struct. The function expects the connections to be
// successfully opened otherwise testing will fail.
func (coord *Coordinator) CreateConnection(
chainA, chainB *TestChain,
clientA, clientB string,
) (*TestConnection, *TestConnection) {
connA, connB, err := coord.ConnOpenInit(chainA, chainB, clientA, clientB)
require.NoError(coord.t, err)
err = coord.ConnOpenTry(chainB, chainA, connB, connA)
require.NoError(coord.t, err)
err = coord.ConnOpenAck(chainA, chainB, connA, connB)
require.NoError(coord.t, err)
err = coord.ConnOpenConfirm(chainB, chainA, connB, connA)
require.NoError(coord.t, err)
return connA, connB
}
// CreateChannel constructs and executes channel handshake messages in order to create
// OPEN channels on chainA and chainB. The function expects the channels to be successfully
// opened otherwise testing will fail.
func (coord *Coordinator) CreateChannel(
chainA, chainB *TestChain,
connA, connB *TestConnection,
order channeltypes.Order,
) (TestChannel, TestChannel) {
channelA, channelB, err := coord.ChanOpenInit(chainA, chainB, connA, connB, order)
require.NoError(coord.t, err)
err = coord.ChanOpenTry(chainB, chainA, channelB, channelA, connB, order)
require.NoError(coord.t, err)
err = coord.ChanOpenAck(chainA, chainB, channelA, channelB)
require.NoError(coord.t, err)
err = coord.ChanOpenConfirm(chainB, chainA, channelB, channelA)
require.NoError(coord.t, err)
return channelA, channelB
}
// SendPacket sends a packet through the channel keeper on the source chain and updates the
// counterparty client for the source chain.
func (coord *Coordinator) SendPacket(
source, counterparty *TestChain,
packet channelexported.PacketI,
counterpartyClientID string,
) error {
if err := source.SendPacket(packet); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// PacketExecuted receives a packet through the channel keeper on the source chain and updates the
// counterparty client for the source chain.
func (coord *Coordinator) PacketExecuted(
source, counterparty *TestChain,
packet channelexported.PacketI,
counterpartyClientID string,
) error {
if err := source.PacketExecuted(packet); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// IncrementTime iterates through all the TestChain's and increments their current header time
// by 5 seconds.
//
// CONTRACT: this function must be called after every commit on any TestChain.
func (coord *Coordinator) IncrementTime() {
for _, chain := range coord.Chains {
chain.CurrentHeader.Time = chain.CurrentHeader.Time.Add(timeIncrement)
chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader})
}
}
// GetChain returns the TestChain using the given chainID and returns an error if it does
// not exist.
func (coord *Coordinator) GetChain(chainID string) *TestChain {
chain, found := coord.Chains[chainID]
require.True(coord.t, found, fmt.Sprintf("%s chain does not exist", chainID))
return chain
}
// GetChainID returns the chainID used for the provided index.
func GetChainID(index int) string {
return ChainIDPrefix + strconv.Itoa(index)
}
// CommitBlock commits a block on the provided indexes and then increments the global time.
//
// CONTRACT: the passed in list of indexes must not contain duplicates
func (coord *Coordinator) CommitBlock(chains ...*TestChain) {
for _, chain := range chains {
chain.App.Commit()
chain.NextBlock()
}
coord.IncrementTime()
}
// CommitNBlocks commits n blocks to state and updates the block height by 1 for each commit.
func (coord *Coordinator) CommitNBlocks(chain *TestChain, n uint64) {
for i := uint64(0); i < n; i++ {
chain.App.BeginBlock(abci.RequestBeginBlock{Header: chain.CurrentHeader})
chain.App.Commit()
chain.NextBlock()
coord.IncrementTime()
}
}
// ConnOpenInit initializes a connection on the source chain with the state INIT
// using the OpenInit handshake call.
//
// NOTE: The counterparty testing connection will be created even if it is not created in the
// application state.
func (coord *Coordinator) ConnOpenInit(
source, counterparty *TestChain,
clientID, counterpartyClientID string,
) (*TestConnection, *TestConnection, error) {
sourceConnection := source.AddTestConnection(clientID, counterpartyClientID)
counterpartyConnection := counterparty.AddTestConnection(counterpartyClientID, clientID)
// initialize connection on source
if err := source.ConnectionOpenInit(counterparty, sourceConnection, counterpartyConnection); err != nil {
return sourceConnection, counterpartyConnection, err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyClientID, clientexported.Tendermint,
); err != nil {
return sourceConnection, counterpartyConnection, err
}
return sourceConnection, counterpartyConnection, nil
}
// ConnOpenTry initializes a connection on the source chain with the state TRYOPEN
// using the OpenTry handshake call.
func (coord *Coordinator) ConnOpenTry(
source, counterparty *TestChain,
sourceConnection, counterpartyConnection *TestConnection,
) error {
// initialize TRYOPEN connection on source
if err := source.ConnectionOpenTry(counterparty, sourceConnection, counterpartyConnection); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyConnection.ClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ConnOpenAck initializes a connection on the source chain with the state OPEN
// using the OpenAck handshake call.
func (coord *Coordinator) ConnOpenAck(
source, counterparty *TestChain,
sourceConnection, counterpartyConnection *TestConnection,
) error {
// set OPEN connection on source using OpenAck
if err := source.ConnectionOpenAck(counterparty, sourceConnection, counterpartyConnection); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyConnection.ClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ConnOpenConfirm initializes a connection on the source chain with the state OPEN
// using the OpenConfirm handshake call.
func (coord *Coordinator) ConnOpenConfirm(
source, counterparty *TestChain,
sourceConnection, counterpartyConnection *TestConnection,
) error {
if err := source.ConnectionOpenConfirm(counterparty, sourceConnection, counterpartyConnection); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyConnection.ClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ChanOpenInit initializes a channel on the source chain with the state INIT
// using the OpenInit handshake call.
//
// NOTE: The counterparty testing channel will be created even if it is not created in the
// application state.
func (coord *Coordinator) ChanOpenInit(
source, counterparty *TestChain,
connection, counterpartyConnection *TestConnection,
order channeltypes.Order,
) (TestChannel, TestChannel, error) {
sourceChannel := connection.AddTestChannel()
counterpartyChannel := counterpartyConnection.AddTestChannel()
// create port capability
source.CreatePortCapability(sourceChannel.PortID)
coord.IncrementTime()
// initialize channel on source
if err := source.ChanOpenInit(sourceChannel, counterpartyChannel, order, connection.ID); err != nil {
return sourceChannel, counterpartyChannel, err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
counterpartyConnection.ClientID, clientexported.Tendermint,
); err != nil {
return sourceChannel, counterpartyChannel, err
}
return sourceChannel, counterpartyChannel, nil
}
// ChanOpenTry initializes a channel on the source chain with the state TRYOPEN
// using the OpenTry handshake call.
func (coord *Coordinator) ChanOpenTry(
source, counterparty *TestChain,
sourceChannel, counterpartyChannel TestChannel,
connection *TestConnection,
order channeltypes.Order,
) error {
// initialize channel on source
if err := source.ChanOpenTry(counterparty, sourceChannel, counterpartyChannel, order, connection.ID); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
connection.CounterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ChanOpenAck initializes a channel on the source chain with the state OPEN
// using the OpenAck handshake call.
func (coord *Coordinator) ChanOpenAck(
source, counterparty *TestChain,
sourceChannel, counterpartyChannel TestChannel,
) error {
if err := source.ChanOpenAck(counterparty, sourceChannel, counterpartyChannel); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
sourceChannel.CounterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ChanOpenConfirm initializes a channel on the source chain with the state OPEN
// using the OpenConfirm handshake call.
func (coord *Coordinator) ChanOpenConfirm(
source, counterparty *TestChain,
sourceChannel, counterpartyChannel TestChannel,
) error {
if err := source.ChanOpenConfirm(counterparty, sourceChannel, counterpartyChannel); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
sourceChannel.CounterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// ChanCloseInit closes a channel on the source chain resulting in the channels state
// being set to CLOSED.
//
// NOTE: does not work with ibc-transfer module
func (coord *Coordinator) ChanCloseInit(
source, counterparty *TestChain,
channel TestChannel,
) error {
if err := source.ChanCloseInit(counterparty, channel); err != nil {
return err
}
coord.IncrementTime()
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
channel.CounterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}
// SetChannelClosed sets a channel state to CLOSED.
func (coord *Coordinator) SetChannelClosed(
source, counterparty *TestChain,
testChannel TestChannel,
) error {
channel := source.GetChannel(testChannel)
channel.State = channeltypes.CLOSED
source.App.IBCKeeper.ChannelKeeper.SetChannel(source.GetContext(), testChannel.PortID, testChannel.ID, channel)
coord.CommitBlock(source)
// update source client on counterparty connection
if err := coord.UpdateClient(
counterparty, source,
testChannel.CounterpartyClientID, clientexported.Tendermint,
); err != nil {
return err
}
return nil
}