From 4d803040d1272508e6d879facd21ad8a8b646aa9 Mon Sep 17 00:00:00 2001 From: Gjermund Garaba Date: Sat, 16 Nov 2024 14:56:37 +0100 Subject: [PATCH] e2e test ibc transfer from rollup to simapp --- testing/interchaintest/cosmos_rollup_test.go | 61 ++++++- testing/interchaintest/go.mod | 6 +- testing/interchaintest/grpc_query.go | 115 ++++++++++++++ testing/interchaintest/setup_test.go | 3 + testing/interchaintest/utils_test.go | 159 +++++++++++++++++++ 5 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 testing/interchaintest/grpc_query.go create mode 100644 testing/interchaintest/utils_test.go diff --git a/testing/interchaintest/cosmos_rollup_test.go b/testing/interchaintest/cosmos_rollup_test.go index 2d26279..2c78b9e 100644 --- a/testing/interchaintest/cosmos_rollup_test.go +++ b/testing/interchaintest/cosmos_rollup_test.go @@ -2,14 +2,22 @@ package interchaintest import ( "fmt" + "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + transfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v9/testing" + "github.com/strangelove-ventures/interchaintest/v8" - "github.com/strangelove-ventures/interchaintest/v8/ibc" "github.com/cosmos/interchain-attestation/core/types" ) @@ -68,16 +76,34 @@ func (s *E2ETestSuite) TestCosmosRollupAttestation() { }, []string{}) s.Require().NoError(err, string(stdOut), string(stdErr)) + // Create an IBC transfer users := interchaintest.GetAndFundTestUsers(s.T(), s.ctx, "ibcuser", math.NewInt(1_000_000), s.rollupsimapp, s.simapp) - rollupUser, simAppUser := users[0], users[1] - transferAmount := ibc.WalletAmount{ - Address: simAppUser.FormattedAddress(), - Denom: s.rollupsimapp.Config().Denom, - Amount: math.NewInt(1_000), - } - _, err = s.rollupsimapp.SendIBCTransfer(s.ctx, "channel-0", rollupUser.KeyName(), transferAmount, ibc.TransferOptions{}) + relayUsers := interchaintest.GetAndFundTestUsers(s.T(), s.ctx, "relayuser", math.NewInt(1_000_000), s.rollupsimapp, s.simapp) + rollupUser, simappUser := users[0], users[1] + _, simappRelayUser := relayUsers[0], relayUsers[1] + transferAmount := math.NewInt(1_000) + timeoutTimestamp := uint64(time.Now().Add(10 * time.Minute).UnixNano()) + + resp, err := s.BroadcastMessages(s.ctx, s.rollupsimapp, rollupUser, 200_000, &transfertypes.MsgTransfer{ + SourcePort: "transfer", + SourceChannel: "channel-0", + Sender: rollupUser.FormattedAddress(), + Receiver: simappUser.FormattedAddress(), + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: timeoutTimestamp, + Memo: "", + Tokens: sdk.NewCoins(sdk.NewCoin(s.rollupsimapp.Config().Denom, transferAmount)), + Forwarding: &transfertypes.Forwarding{}, + }) + s.Require().NoError(err) + + packet, err := ibctesting.ParsePacketFromEvents(resp.Events) s.Require().NoError(err) + // Wait for validators to catch up + time.Sleep(5 * time.Second) + + // Check that all sidecars have the attestation for i, val := range s.simapp.Validators { s.Require().Len(val.Sidecars, 1) sidecar := val.Sidecars[0] @@ -98,4 +124,23 @@ func (s *E2ETestSuite) TestCosmosRollupAttestation() { s.Require().Equal([]byte(fmt.Sprintf("attestator-%d", i)), resp.Attestations[0].AttestatorId) s.Require().Len(resp.Attestations[0].AttestedData.PacketCommitments, 1) } + + // Receive packet + _, err = s.BroadcastMessages(s.ctx, s.simapp, simappRelayUser, 200_000, &channeltypes.MsgRecvPacket{ + Packet: packet, + ProofCommitment: []byte("not-used"), + ProofHeight: clienttypes.Height{}, + Signer: simappRelayUser.FormattedAddress(), + }) + s.Require().NoError(err) + + // Check balance on simapp + denomOnSimapp := transfertypes.NewDenom(s.rollupsimapp.Config().Denom, transfertypes.NewHop("transfer", "channel-0")) + balanceResp, err := GRPCQuery[banktypes.QueryBalanceResponse](s.ctx, s.simapp, &banktypes.QueryBalanceRequest{ + Address: simappUser.FormattedAddress(), + Denom: denomOnSimapp.IBCDenom(), + }) + s.Require().NoError(err) + s.Require().NotNil(balanceResp.Balance) + s.Require().Equal(transferAmount.Int64(), balanceResp.Balance.Amount.Int64()) } diff --git a/testing/interchaintest/go.mod b/testing/interchaintest/go.mod index 62d0b05..cb4213f 100644 --- a/testing/interchaintest/go.mod +++ b/testing/interchaintest/go.mod @@ -19,7 +19,9 @@ replace ( require ( cosmossdk.io/math v1.3.0 + github.com/cometbft/cometbft v0.38.10 github.com/cosmos/cosmos-sdk v0.50.9 + github.com/cosmos/ibc-go/v9 v9.0.0-beta.1 github.com/cosmos/interchain-attestation/core v0.0.0 github.com/cosmos/interchain-attestation/sidecar v0.0.0 github.com/ethereum/go-ethereum v1.14.7 @@ -38,6 +40,7 @@ require ( cloud.google.com/go/iam v1.1.9 // indirect cloud.google.com/go/storage v1.41.0 // indirect cosmossdk.io/api v0.7.5 // indirect + cosmossdk.io/client/v2 v2.0.0-beta.3 // indirect cosmossdk.io/collections v0.4.0 // indirect cosmossdk.io/core v0.11.1 // indirect cosmossdk.io/depinject v1.0.0 // indirect @@ -65,13 +68,13 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v1.1.1 // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect - github.com/cometbft/cometbft v0.38.10 // indirect github.com/cometbft/cometbft-db v0.12.0 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect @@ -84,7 +87,6 @@ require ( github.com/cosmos/gogoproto v1.5.0 // indirect github.com/cosmos/iavl v1.2.0 // indirect github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect - github.com/cosmos/ibc-go/v9 v9.0.0-beta.1 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect diff --git a/testing/interchaintest/grpc_query.go b/testing/interchaintest/grpc_query.go new file mode 100644 index 0000000..f1e80d9 --- /dev/null +++ b/testing/interchaintest/grpc_query.go @@ -0,0 +1,115 @@ +package interchaintest + +import ( + "context" + "fmt" + + "github.com/cosmos/gogoproto/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + pb "google.golang.org/protobuf/proto" + + msgv1 "cosmossdk.io/api/cosmos/msg/v1" + reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" +) + +var queryReqToPath = make(map[string]string) + +func populateQueryReqToPath(ctx context.Context, chain *cosmos.CosmosChain) error { + resp, err := queryFileDescriptors(ctx, chain) + if err != nil { + return err + } + + for _, fileDescriptor := range resp.Files { + for _, service := range fileDescriptor.GetService() { + // Skip services that are annotated with the "cosmos.msg.v1.service" option. + if ext := pb.GetExtension(service.GetOptions(), msgv1.E_Service); ext != nil && ext.(bool) { + continue + } + + for _, method := range service.GetMethod() { + // trim the first character from input which is a dot + queryReqToPath[method.GetInputType()[1:]] = fileDescriptor.GetPackage() + "." + service.GetName() + "/" + method.GetName() + } + } + } + + return nil +} + +func ABCIQuery(ctx context.Context, chain *cosmos.CosmosChain, req *abci.RequestQuery) (*abci.ResponseQuery, error) { + // Create a connection to the gRPC server. + grpcConn, err := grpc.Dial( + chain.GetHostGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return &abci.ResponseQuery{}, err + } + + defer grpcConn.Close() + + resp := &abci.ResponseQuery{} + err = grpcConn.Invoke(ctx, "cosmos.base.tendermint.v1beta1.Service/ABCIQuery", req, resp) + if err != nil { + return &abci.ResponseQuery{}, err + } + + return resp, nil +} + +// Queries the chain with a query request and deserializes the response to T +func GRPCQuery[T any](ctx context.Context, chain *cosmos.CosmosChain, req proto.Message, opts ...grpc.CallOption) (*T, error) { + path, ok := queryReqToPath[proto.MessageName(req)] + if !ok { + return nil, fmt.Errorf("no path found for %s", proto.MessageName(req)) + } + + // Create a connection to the gRPC server. + grpcConn, err := grpc.Dial( + chain.GetHostGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, err + } + + defer grpcConn.Close() + + resp := new(T) + err = grpcConn.Invoke(ctx, path, req, resp, opts...) + if err != nil { + return nil, err + } + + return resp, nil +} + +func queryFileDescriptors(ctx context.Context, chain *cosmos.CosmosChain) (*reflectionv1.FileDescriptorsResponse, error) { + // Create a connection to the gRPC server. + grpcConn, err := grpc.Dial( + chain.GetHostGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, err + } + + defer grpcConn.Close() + + resp := new(reflectionv1.FileDescriptorsResponse) + err = grpcConn.Invoke( + ctx, reflectionv1.ReflectionService_FileDescriptors_FullMethodName, + &reflectionv1.FileDescriptorsRequest{}, resp, + ) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/testing/interchaintest/setup_test.go b/testing/interchaintest/setup_test.go index ddc063e..80124fa 100644 --- a/testing/interchaintest/setup_test.go +++ b/testing/interchaintest/setup_test.go @@ -113,6 +113,9 @@ func (s *E2ETestSuite) SetupSuite() { }) s.Require().NoError(err) + err = populateQueryReqToPath(s.ctx, s.simapp) + s.Require().NoError(err) + s.setupSidecars() // Create relayer users diff --git a/testing/interchaintest/utils_test.go b/testing/interchaintest/utils_test.go new file mode 100644 index 0000000..a5daf86 --- /dev/null +++ b/testing/interchaintest/utils_test.go @@ -0,0 +1,159 @@ +package interchaintest + +import ( + "context" + "encoding/hex" + "fmt" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/cometbft/cometbft/abci/types" + + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testutil" +) + +// BroadcastMessages broadcasts the provided messages to the given chain and signs them on behalf of the provided user. +// Once the broadcast response is returned, we wait for two blocks to be created on chain. +func (s *E2ETestSuite) BroadcastMessages(ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, gas uint64, msgs ...sdk.Msg) (*sdk.TxResponse, error) { + sdk.GetConfig().SetBech32PrefixForAccount(chain.Config().Bech32Prefix, chain.Config().Bech32Prefix+sdk.PrefixPublic) + sdk.GetConfig().SetBech32PrefixForValidator( + chain.Config().Bech32Prefix+sdk.PrefixValidator+sdk.PrefixOperator, + chain.Config().Bech32Prefix+sdk.PrefixValidator+sdk.PrefixOperator+sdk.PrefixPublic, + ) + + broadcaster := cosmos.NewBroadcaster(s.T(), chain) + + broadcaster.ConfigureClientContextOptions(func(clientContext client.Context) client.Context { + return clientContext. + WithCodec(chain.Config().EncodingConfig.Codec). + WithChainID(chain.Config().ChainID). + WithTxConfig(chain.Config().EncodingConfig.TxConfig) + }) + + broadcaster.ConfigureFactoryOptions(func(factory tx.Factory) tx.Factory { + return factory.WithGas(gas) + }) + + resp, err := cosmos.BroadcastTx(ctx, broadcaster, user, msgs...) + if err != nil { + return nil, err + } + + // wait for 2 blocks for the transaction to be included + s.Require().NoError(testutil.WaitForBlocks(ctx, 2, chain)) + + if resp.Code != 0 { + return nil, fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.RawLog) + } + + return &resp, nil +} + +// TODO: Replace with ibc-go/v9/testing when possible +// ParsePacketFromEvents parses events emitted from a MsgRecvPacket and returns +// the first packet found. +// Returns an error if no packet is found. +func ParsePacketFromEvents(events []abci.Event) (channeltypes.Packet, error) { + packets, err := ParsePacketsFromEvents(events) + if err != nil { + return channeltypes.Packet{}, err + } + return packets[0], nil +} + +// ParsePacketsFromEvents parses events emitted from a MsgRecvPacket and returns +// all the packets found. +// Returns an error if no packet is found. +func ParsePacketsFromEvents(events []abci.Event) ([]channeltypes.Packet, error) { + ferr := func(err error) ([]channeltypes.Packet, error) { + return nil, fmt.Errorf("ibctesting.ParsePacketsFromEvents: %w", err) + } + var packets []channeltypes.Packet + for _, ev := range events { + if ev.Type == channeltypes.EventTypeSendPacket { + var packet channeltypes.Packet + for _, attr := range ev.Attributes { + switch attr.Key { + case channeltypes.AttributeKeyDataHex: + data, err := hex.DecodeString(attr.Value) + if err != nil { + return ferr(err) + } + packet.Data = data + case channeltypes.AttributeKeySequence: + seq, err := strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return ferr(err) + } + + packet.Sequence = seq + + case channeltypes.AttributeKeySrcPort: + packet.SourcePort = attr.Value + + case channeltypes.AttributeKeySrcChannel: + packet.SourceChannel = attr.Value + + case channeltypes.AttributeKeyDstPort: + packet.DestinationPort = attr.Value + + case channeltypes.AttributeKeyDstChannel: + packet.DestinationChannel = attr.Value + + case channeltypes.AttributeKeyTimeoutHeight: + height, err := clienttypes.ParseHeight(attr.Value) + if err != nil { + return ferr(err) + } + + packet.TimeoutHeight = height + + case channeltypes.AttributeKeyTimeoutTimestamp: + timestamp, err := strconv.ParseUint(attr.Value, 10, 64) + if err != nil { + return ferr(err) + } + + packet.TimeoutTimestamp = timestamp + + default: + continue + } + } + + packets = append(packets, packet) + } + } + if len(packets) == 0 { + return ferr(fmt.Errorf("acknowledgement event attribute not found")) + } + return packets, nil +} + +// ParseAckFromEvents parses events emitted from a MsgRecvPacket and returns the +// acknowledgement. +func ParseAckFromEvents(events []abci.Event) ([]byte, error) { + for _, ev := range events { + if ev.Type == channeltypes.EventTypeWriteAck { + for _, attr := range ev.Attributes { + if attr.Key == channeltypes.AttributeKeyAckHex { + value, err := hex.DecodeString(attr.Value) + if err != nil { + return nil, err + } + + return value, nil + } + } + } + } + return nil, fmt.Errorf("acknowledgement event attribute not found") +}