diff --git a/.github/workflows/wormchain-icts.yml b/.github/workflows/wormchain-icts.yml new file mode 100644 index 0000000000..7c71430a85 --- /dev/null +++ b/.github/workflows/wormchain-icts.yml @@ -0,0 +1,63 @@ +name: Wormchain's end-to-end Interchain Tests + +on: + pull_request: + push: + tags: + - "**" + branches: + - "main" + +permissions: + contents: read + packages: write + +env: + GO_VERSION: 1.21 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e-tests: + runs-on: ubuntu-latest + strategy: + matrix: + # names of `make` commands to run tests + test: + - "ictest-cancel-upgrade" + - "ictest-upgrade" + - "ictest-wormchain" + - "ictest-ibc-receiver" + fail-fast: false + + steps: + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: interchaintest/go.sum + + - name: checkout chain + uses: actions/checkout@v4 + + - name: Run Test + id: run_test + continue-on-error: true + working-directory: wormchain + run: make ${{ matrix.test }} + + - name: Retry Failed Test + if: steps.run_test.outcome == 'failure' + working-directory: wormchain + run: | + for i in 1 2; do + echo "Retry attempt $i" + if make ${{ matrix.test }}; then + echo "Test passed on retry" + exit 0 + fi + done + echo "Test failed after retries" + exit 1 diff --git a/wormchain/Makefile b/wormchain/Makefile index 60b01c59ce..8252663763 100644 --- a/wormchain/Makefile +++ b/wormchain/Makefile @@ -78,3 +78,31 @@ bootstrap: clean: rm -rf build/wormchaind build/wormchaind-* build/**/*.db build/**/*.wal vue echo "{\"height\":\"0\",\"round\":0,\"step\":0}" > build/data/priv_validator_state.json + +##################### +## INTERCHAINTESTS ## +##################### + +# Individual Tests ($$ is interpreted as $) +rm-testcache: + go clean -testcache + +ictest-cancel-upgrade: rm-testcache + cd interchaintest && go test -race -v -run ^TestCancelUpgrade$$ ./... + +ictest-malformed-payload: rm-testcache + cd interchaintest && go test -race -v -run ^TestMalformedPayload$$ ./... + +ictest-upgrade-failure: rm-testcache + cd interchaintest && go test -race -v -run ^TestUpgradeFailure$$ ./... + +ictest-upgrade: rm-testcache + cd interchaintest && go test -race -v -run ^TestUpgrade$$ ./... + +ictest-wormchain: rm-testcache + cd interchaintest && go test -race -v -run ^TestWormchain$$ ./... + +ictest-ibc-receiver: rm-testcache + cd interchaintest && go test -race -v -run ^TestIbcReceiver ./... + +.PHONY: ictest-cancel-upgrade ictest-malformed-payload ictest-upgrade-failure ictest-upgrade ictest-wormchain ictest-ibc-receiver \ No newline at end of file diff --git a/wormchain/interchaintest/contracts/wormchain_ibc_receiver.wasm b/wormchain/interchaintest/contracts/wormchain_ibc_receiver.wasm new file mode 100644 index 0000000000..72e4cd7bc4 Binary files /dev/null and b/wormchain/interchaintest/contracts/wormchain_ibc_receiver.wasm differ diff --git a/wormchain/interchaintest/contracts/wormhole_ibc.wasm b/wormchain/interchaintest/contracts/wormhole_ibc.wasm new file mode 100644 index 0000000000..27bbe3f94a Binary files /dev/null and b/wormchain/interchaintest/contracts/wormhole_ibc.wasm differ diff --git a/wormchain/interchaintest/go.mod b/wormchain/interchaintest/go.mod index 428ead1b1d..9944964aef 100644 --- a/wormchain/interchaintest/go.mod +++ b/wormchain/interchaintest/go.mod @@ -17,6 +17,7 @@ require ( github.com/ethereum/go-ethereum v1.11.6 github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230815125617-67bc301715ea github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 + github.com/tendermint/tendermint v0.34.26 github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230614161948-7f6213019abf ) @@ -163,7 +164,6 @@ require ( github.com/subosito/gotenv v1.4.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tendermint/tendermint v0.34.26 // indirect github.com/tendermint/tm-db v0.6.7 // indirect github.com/tidwall/btree v1.5.0 // indirect github.com/vedhavyas/go-subkey v1.0.3 // indirect diff --git a/wormchain/interchaintest/helpers/instantiate_contract.go b/wormchain/interchaintest/helpers/instantiate_contract.go index 9a0c427f60..e627f02865 100644 --- a/wormchain/interchaintest/helpers/instantiate_contract.go +++ b/wormchain/interchaintest/helpers/instantiate_contract.go @@ -69,3 +69,19 @@ func InstantiateContract( type QueryContractResponse struct { Contracts []string `json:"contracts"` } + +func StoreAndInstantiateWormholeContract( + t *testing.T, + ctx context.Context, + chain *cosmos.CosmosChain, + keyName string, + fileLoc string, + label string, + message string, + guardians *guardians.ValSet, +) (contract ContractInfoResponse) { + codeId := StoreContract(t, ctx, chain, keyName, fileLoc, guardians) + contractAddr := InstantiateContract(t, ctx, chain, keyName, codeId, label, message, guardians) + + return QueryContractInfo(t, chain, ctx, contractAddr) +} diff --git a/wormchain/interchaintest/helpers/query_contract_info.go b/wormchain/interchaintest/helpers/query_contract_info.go new file mode 100644 index 0000000000..f427cea45a --- /dev/null +++ b/wormchain/interchaintest/helpers/query_contract_info.go @@ -0,0 +1,40 @@ +package helpers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/stretchr/testify/require" +) + +// QueryContractInfo queries the information about a contract like the admin and code_id. +func QueryContractInfo(t *testing.T, chain *cosmos.CosmosChain, ctx context.Context, contractAddress string) ContractInfoResponse { + stdout, _, err := chain.GetFullNode().ExecQuery(ctx, + "wasm", "contract", contractAddress, + ) + require.NoError(t, err) + + res := new(ContractInfoResponse) + err = json.Unmarshal(stdout, res) + require.NoError(t, err) + + return *res +} + +type ContractInfoResponse struct { + Address string `json:"address"` + ContractInfo struct { + CodeID string `json:"code_id"` + Creator string `json:"creator"` + Admin string `json:"admin"` + Label string `json:"label"` + Created struct { + BlockHeight string `json:"block_height"` + TxIndex string `json:"tx_index"` + } `json:"created"` + IbcPortID string `json:"ibc_port_id"` + Extension any `json:"extension"` + } `json:"contract_info"` +} diff --git a/wormchain/interchaintest/helpers/store_contract.go b/wormchain/interchaintest/helpers/store_contract.go index b26665eafd..5151ebadd1 100644 --- a/wormchain/interchaintest/helpers/store_contract.go +++ b/wormchain/interchaintest/helpers/store_contract.go @@ -61,11 +61,6 @@ func createIbcReceiverUpdateChannelPayload(payload vaa.BodyIbcUpdateChannelChain return gov_msg.MarshalBinary(), nil } -// func UpgradeCoreContract(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, keyName string, payload vaa.BodyContractUpgrade, guardians *guardians.ValSet) { -// node := chain.GetFullNode() - -// } - // wormchaind tx wormhole store [wasm file] [vaa-hex] [flags] func StoreContract(t *testing.T, ctx context.Context, chain *cosmos.CosmosChain, keyName string, fileLoc string, guardians *guardians.ValSet) (codeId string) { node := chain.GetFullNode() diff --git a/wormchain/interchaintest/helpers/utils.go b/wormchain/interchaintest/helpers/utils.go index b86e11afe7..d9d94fcaed 100644 --- a/wormchain/interchaintest/helpers/utils.go +++ b/wormchain/interchaintest/helpers/utils.go @@ -1,14 +1,21 @@ package helpers import ( + "context" "fmt" + "slices" + "strconv" "strings" "testing" "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + abcitypes "github.com/tendermint/tendermint/abci/types" ) func MustAccAddressFromBech32(address string, bech32Prefix string) sdk.AccAddress { @@ -45,3 +52,92 @@ func FindEventAttribute(t *testing.T, chain *cosmos.CosmosChain, txHash string, fmt.Println("Not found: ", eventType, " ", attributeKey, " ", attributeValue, "!") return false } + +// FindOpenChannelByVersion queries all the channels of a given chain and returns the first with the given version. If no channel is found, it will fail the test. +func FindOpenChannelByVersion( + t *testing.T, + ctx context.Context, + eRep *testreporter.RelayerExecReporter, + r ibc.Relayer, + chain *cosmos.CosmosChain, + version string) ibc.ChannelOutput { + // iterate up to 20 times to allow for chain to catch up + for i := 0; i < 20; i++ { + + channels, err := r.GetChannels(ctx, eRep, chain.Config().ChainID) + require.NoError(t, err) + + channelIdx := slices.IndexFunc(channels, func(channel ibc.ChannelOutput) bool { + return channel.State == "STATE_OPEN" && channel.Version == version + }) + if channelIdx != -1 { + return channels[channelIdx] + } + testutil.WaitForBlocks(ctx, 1, chain) + } + + require.Failf(t, "channel with version %s not found", version) + return ibc.ChannelOutput{} +} + +func GetIBCTx( + c *cosmos.CosmosChain, + txHash string, +) (tx ibc.Tx, _ error) { + txResp, err := c.GetTransaction(txHash) + if err != nil { + return tx, fmt.Errorf("failed to get transaction %s: %w", txHash, err) + } + tx.Height = uint64(txResp.Height) + tx.TxHash = txHash + // In cosmos, user is charged for entire gas requested, not the actual gas used. + tx.GasSpent = txResp.GasWanted + + const evType = "send_packet" + events := txResp.Events + + var ( + seq, _ = AttributeValue(events, evType, "packet_sequence") + srcPort, _ = AttributeValue(events, evType, "packet_src_port") + srcChan, _ = AttributeValue(events, evType, "packet_src_channel") + dstPort, _ = AttributeValue(events, evType, "packet_dst_port") + dstChan, _ = AttributeValue(events, evType, "packet_dst_channel") + timeoutHeight, _ = AttributeValue(events, evType, "packet_timeout_height") + timeoutTs, _ = AttributeValue(events, evType, "packet_timeout_timestamp") + data, _ = AttributeValue(events, evType, "packet_data") + ) + tx.Packet.SourcePort = srcPort + tx.Packet.SourceChannel = srcChan + tx.Packet.DestPort = dstPort + tx.Packet.DestChannel = dstChan + tx.Packet.TimeoutHeight = timeoutHeight + tx.Packet.Data = []byte(data) + + seqNum, err := strconv.Atoi(seq) + if err != nil { + return tx, fmt.Errorf("invalid packet sequence from events %s: %w", seq, err) + } + tx.Packet.Sequence = uint64(seqNum) + + timeoutNano, err := strconv.ParseUint(timeoutTs, 10, 64) + if err != nil { + return tx, fmt.Errorf("invalid packet timestamp timeout %s: %w", timeoutTs, err) + } + tx.Packet.TimeoutTimestamp = ibc.Nanoseconds(timeoutNano) + + return tx, nil +} + +func AttributeValue(events []abcitypes.Event, eventType, attrKey string) (string, bool) { + for _, event := range events { + if event.Type != eventType { + continue + } + for _, attr := range event.Attributes { + if string(attr.Key) == attrKey { + return string(attr.Value), true + } + } + } + return "", false +} diff --git a/wormchain/interchaintest/helpers/vaa.go b/wormchain/interchaintest/helpers/vaa.go index 452bbce52d..093b84e920 100644 --- a/wormchain/interchaintest/helpers/vaa.go +++ b/wormchain/interchaintest/helpers/vaa.go @@ -33,3 +33,14 @@ func generateVaa(index uint32, signers *guardians.ValSet, emitterChain vaa.Chain latestSequence = latestSequence + 1 return signVaa(v, signers) } + +func GenerateGovernanceVaa(index uint32, + signers *guardians.ValSet, + payload []byte) vaa.VAA { + + v := vaa.CreateGovernanceVAA(time.Unix(0, 0), + uint32(1), uint64(latestSequence), index, payload) + + latestSequence = latestSequence + 1 + return signVaa(*v, signers) +} diff --git a/wormchain/interchaintest/helpers/wormchain_ibc_receiver/helpers.go b/wormchain/interchaintest/helpers/wormchain_ibc_receiver/helpers.go new file mode 100644 index 0000000000..1ccac990d7 --- /dev/null +++ b/wormchain/interchaintest/helpers/wormchain_ibc_receiver/helpers.go @@ -0,0 +1,61 @@ +package wormchain_ibc_receiver + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "github.com/wormhole-foundation/wormchain/interchaintest/guardians" + "github.com/wormhole-foundation/wormchain/interchaintest/helpers" + "github.com/wormhole-foundation/wormhole/sdk/vaa" +) + +func SubmitIbcReceiverUpdateChannelChainMsg(t *testing.T, + allowlistChainID vaa.ChainID, + allowlistChannel string, + guardians *guardians.ValSet) string { + + paddedChannel, _ := vaa.LeftPadIbcChannelId(allowlistChannel) + + bodyIbcReceiverUpdateChannelChain := vaa.BodyIbcUpdateChannelChain{ + TargetChainId: vaa.ChainIDWormchain, + ChannelId: paddedChannel, + ChainId: allowlistChainID, + } + + payload, err := bodyIbcReceiverUpdateChannelChain.Serialize(vaa.IbcReceiverModuleStr) + require.NoError(t, err) + + v := helpers.GenerateGovernanceVaa(0, guardians, payload) + vBz, err := v.Marshal() + require.NoError(t, err) + vHex := base64.StdEncoding.EncodeToString(vBz) + + var vaas [1]Binary + vaas[0] = Binary(vHex) + + submitVAAMsg := ExecuteMsg{ + SubmitUpdateChannelChain: &ExecuteMsg_SubmitUpdateChannelChain{ + Vaas: vaas[:], + }, + } + + submitVAAMsgBz, err := json.Marshal(submitVAAMsg) + require.NoError(t, err) + + return string(submitVAAMsgBz) +} + +type ReceiverAck struct { + Ok *struct{} `json:"ok,omitempty"` + Error string `json:"error,omitempty"` +} + +func (r ReceiverAck) IsOk() bool { + return len(r.Error) == 0 +} + +func (r ReceiverAck) IsError() bool { + return len(r.Error) > 0 +} diff --git a/wormchain/interchaintest/helpers/wormchain_ibc_receiver/wormchain_ibc_receiver.go b/wormchain/interchaintest/helpers/wormchain_ibc_receiver/wormchain_ibc_receiver.go new file mode 100644 index 0000000000..bc302ff204 --- /dev/null +++ b/wormchain/interchaintest/helpers/wormchain_ibc_receiver/wormchain_ibc_receiver.go @@ -0,0 +1,40 @@ +/* Code generated by github.com/srdtrk/go-codegen, DO NOT EDIT. */ +package wormchain_ibc_receiver + +type InstantiateMsg struct{} + +type ExecuteMsg struct { + // Submit one or more signed VAAs to update the on-chain state. If processing any of the VAAs returns an error, the entire transaction is aborted and none of the VAAs are committed. + SubmitUpdateChannelChain *ExecuteMsg_SubmitUpdateChannelChain `json:"submit_update_channel_chain,omitempty"` +} + +// Contract queries +type QueryMsg struct { + AllChannelChains *QueryMsg_AllChannelChains `json:"all_channel_chains,omitempty"` + ChannelChain *QueryMsg_ChannelChain `json:"channel_chain,omitempty"` +} + +type ExecuteMsg_SubmitUpdateChannelChain struct { + // One or more VAAs to be submitted. Each VAA should be encoded in the standard wormhole wire format. + Vaas []Binary `json:"vaas"` +} + +/* +Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline. +This is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also . +*/ +type Binary string + +type QueryMsg_AllChannelChains struct{} + +type QueryMsg_ChannelChain struct { + ChannelId Binary `json:"channel_id"` +} + +type ChannelChainResponse struct { + ChainId int `json:"chain_id"` +} + +type AllChannelChainsResponse struct { + ChannelsChains []any `json:"channels_chains"` +} diff --git a/wormchain/interchaintest/helpers/wormhole_ibc/helpers.go b/wormchain/interchaintest/helpers/wormhole_ibc/helpers.go new file mode 100644 index 0000000000..6a60eab72d --- /dev/null +++ b/wormchain/interchaintest/helpers/wormhole_ibc/helpers.go @@ -0,0 +1,45 @@ +package wormhole_ibc + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "github.com/wormhole-foundation/wormchain/interchaintest/guardians" + "github.com/wormhole-foundation/wormchain/interchaintest/helpers" + "github.com/wormhole-foundation/wormhole/sdk/vaa" +) + +func SubmitWormholeIbcUpdateChannelChainMsg(t *testing.T, + allowlistChainID vaa.ChainID, + allowlistChannel string, + guardians *guardians.ValSet) string { + + paddedChannel, _ := vaa.LeftPadIbcChannelId(allowlistChannel) + + bodyIbcReceiverUpdateChannelChain := vaa.BodyIbcUpdateChannelChain{ + TargetChainId: vaa.ChainIDWormchain, + ChannelId: paddedChannel, + ChainId: allowlistChainID, + } + + payload, err := bodyIbcReceiverUpdateChannelChain.Serialize(vaa.IbcReceiverModuleStr) + require.NoError(t, err) + + v := helpers.GenerateGovernanceVaa(0, guardians, payload) + vBz, err := v.Marshal() + require.NoError(t, err) + vHex := base64.StdEncoding.EncodeToString(vBz) + + submitVAAMsg := ExecuteMsg{ + SubmitVAA: nil, + PostMessage: nil, + SubmitUpdateChannelChain: &ExecuteMsg_SubmitUpdateChannelChain{Vaa: Binary(vHex)}, + } + + submitVAAMsgBz, err := json.Marshal(submitVAAMsg) + require.NoError(t, err) + + return string(submitVAAMsgBz) +} diff --git a/wormchain/interchaintest/helpers/wormhole_ibc/wormhole_ibc.go b/wormchain/interchaintest/helpers/wormhole_ibc/wormhole_ibc.go new file mode 100644 index 0000000000..02c3704e3d --- /dev/null +++ b/wormchain/interchaintest/helpers/wormhole_ibc/wormhole_ibc.go @@ -0,0 +1,111 @@ +/* Code generated by github.com/srdtrk/go-codegen, DO NOT EDIT. */ +package wormhole_ibc + +// The instantiation parameters of the core bridge contract. See [`crate::state::ConfigInfo`] for more details on what these fields mean. +type InstantiateMsg struct { + FeeDenom string `json:"fee_denom"` + GovAddress Binary `json:"gov_address"` + GovChain int `json:"gov_chain"` + GuardianSetExpirity int `json:"guardian_set_expirity"` + // Guardian set to initialise the contract with. + InitialGuardianSet GuardianSetInfo `json:"initial_guardian_set"` + ChainId int `json:"chain_id"` +} + +type ExecuteMsg struct { + SubmitVAA *ExecuteMsg_SubmitVAA `json:"submit_v_a_a,omitempty"` + PostMessage *ExecuteMsg_PostMessage `json:"post_message,omitempty"` + // Submit a signed VAA to update the on-chain state. If processing any of the VAAs returns an error, the entire transaction is aborted and none of the VAAs are committed. + SubmitUpdateChannelChain *ExecuteMsg_SubmitUpdateChannelChain `json:"submit_update_channel_chain,omitempty"` +} + +type QueryMsg struct { + GuardianSetInfo *QueryMsg_GuardianSetInfo `json:"guardian_set_info,omitempty"` + VerifyVAA *QueryMsg_VerifyVAA `json:"verify_v_a_a,omitempty"` + GetState *QueryMsg_GetState `json:"get_state,omitempty"` + QueryAddressHex *QueryMsg_QueryAddressHex `json:"query_address_hex,omitempty"` +} + +type ExecuteMsg_PostMessage struct { + Message Binary `json:"message"` + Nonce int `json:"nonce"` +} + +type QueryMsg_GuardianSetInfo struct{} + +type GetStateResponse struct { + Fee Coin `json:"fee"` +} + +type GuardianSetInfoResponse struct { + Addresses []GuardianAddress `json:"addresses"` + GuardianSetIndex int `json:"guardian_set_index"` +} + +type ExecuteMsg_SubmitVAA struct { + Vaa Binary `json:"vaa"` +} + +type Coin struct { + Amount Uint128 `json:"amount"` + Denom string `json:"denom"` +} + +/* +A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq. +# Examples +Use `from` to create instances of this and `u128` to get the value out: +``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123); +let b = Uint128::from(42u64); assert_eq!(b.u128(), 42); +let c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ``` +*/ +type Uint128 string + +type GetAddressHexResponse struct { + Hex string `json:"hex"` +} + +type ParsedVAA struct { + Hash []int `json:"hash"` + Nonce int `json:"nonce"` + Payload []int `json:"payload"` + Sequence int `json:"sequence"` + ConsistencyLevel int `json:"consistency_level"` + EmitterAddress []int `json:"emitter_address"` + LenSigners int `json:"len_signers"` + Timestamp int `json:"timestamp"` + Version int `json:"version"` + EmitterChain int `json:"emitter_chain"` + GuardianSetIndex int `json:"guardian_set_index"` +} + +type ExecuteMsg_SubmitUpdateChannelChain struct { + // VAA to submit. The VAA should be encoded in the standard wormhole wire format. + Vaa Binary `json:"vaa"` +} + +type QueryMsg_VerifyVAA struct { + BlockTime int `json:"block_time"` + Vaa Binary `json:"vaa"` +} + +/* +Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline. +This is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also . +*/ +type Binary string + +type QueryMsg_GetState struct{} + +type QueryMsg_QueryAddressHex struct { + Address string `json:"address"` +} + +type GuardianAddress struct { + Bytes []byte `json:"bytes"` +} + +type GuardianSetInfo struct { + Addresses []GuardianAddress `json:"addresses"` + ExpirationTime uint64 `json:"expiration_time"` +} diff --git a/wormchain/interchaintest/ibc_receiver_test.go b/wormchain/interchaintest/ibc_receiver_test.go new file mode 100644 index 0000000000..38ec704d49 --- /dev/null +++ b/wormchain/interchaintest/ibc_receiver_test.go @@ -0,0 +1,366 @@ +package ictest + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/docker/docker/client" + "github.com/strangelove-ventures/interchaintest/v4" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos/wasm" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/relayer" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "go.uber.org/zap/zaptest" + + "github.com/stretchr/testify/require" + + "github.com/wormhole-foundation/wormchain/interchaintest/guardians" + "github.com/wormhole-foundation/wormchain/interchaintest/helpers" + "github.com/wormhole-foundation/wormchain/interchaintest/helpers/wormchain_ibc_receiver" + "github.com/wormhole-foundation/wormchain/interchaintest/helpers/wormhole_ibc" + "github.com/wormhole-foundation/wormhole/sdk/vaa" +) + +const CUSTOM_IBC_VERSION string = "ibc-wormhole-v1" + +func createChains(t *testing.T, wormchainVersion string, guardians guardians.ValSet) []ibc.Chain { + numWormchainVals := len(guardians.Vals) + wormchainConfig.Images[0].Version = wormchainVersion + + // Create chain factory with wormchain + wormchainConfig.ModifyGenesis = ModifyGenesis(votingPeriod, maxDepositPeriod, guardians) + + cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ + { + ChainName: "wormchain", + ChainConfig: wormchainConfig, + NumValidators: &numWormchainVals, + NumFullNodes: &numFullNodes, + }, + { + Name: "osmosis", + Version: "v15.1.2", + ChainConfig: ibc.ChainConfig{ + ChainID: "osmosis-1002", // hardcoded handling in osmosis binary for osmosis-1, so need to override to something different. + GasPrices: "1.0uosmo", + EncodingConfig: wasm.WasmEncoding(), + }, + }, + }) + + // Get chains from the chain factory + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + return chains +} + +func buildInterchain(t *testing.T, chains []ibc.Chain) (context.Context, ibc.Relayer, *testreporter.RelayerExecReporter, *client.Client) { + // Create a new Interchain object which describes the chains, relayers, and IBC connections we want to use + ic := interchaintest.NewInterchain() + + for _, chain := range chains { + ic.AddChain(chain) + } + + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + + wormOsmoPath := "wormosmo" + ctx := context.Background() + client, network := interchaintest.DockerSetup(t) + r := interchaintest.NewBuiltinRelayerFactory(ibc.CosmosRly, zaptest.NewLogger(t), + relayer.StartupFlags("-b", "100"), + relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.5.2", "100:1000")).Build( + t, client, network) + ic.AddRelayer(r, "relayer") + + ic.AddLink(interchaintest.InterchainLink{ + Chain1: chains[1], // Osmosis + Chain2: chains[0], // Wormchain + Relayer: r, + Path: wormOsmoPath, + }) + + err := ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + SkipPathCreation: false, + BlockDatabaseFile: interchaintest.DefaultBlockDatabaseFilepath(), + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = ic.Close() + }) + + // Start the relayer + err = r.StartRelayer(ctx, eRep, wormOsmoPath) + require.NoError(t, err) + + //interchaintest.TempDir(sui) + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("an error occured while stopping the relayer: %s", err) + } + }, + ) + + return ctx, r, eRep, client +} + +func TestIbcReceiverHappyPath(t *testing.T) { + // Base setup + numVals := 2 + guardians := guardians.CreateValSet(t, numVals) + chains := createChains(t, "v2.24.2", *guardians) + ctx, r, eRep, _ := buildInterchain(t, chains) + + // Chains + wormchain := chains[0].(*cosmos.CosmosChain) + osmosis := chains[1].(*cosmos.CosmosChain) + + // Instantiate the wormchain-ibc-receiver and wormhole-ibc contracts + wormchainReceiverContractInfo, osmosisSenderContractInfo := instantiateWormholeIbcContracts(t, ctx, wormchain, osmosis, guardians) + + // Spin up a new channel for the contracts to communicate over (this new channel will need to be whitelisted on the wormhole-ibc contract) + err := r.LinkPath(ctx, eRep, "wormosmo", ibc.CreateChannelOptions{ + SourcePortName: osmosisSenderContractInfo.ContractInfo.IbcPortID, + DestPortName: wormchainReceiverContractInfo.ContractInfo.IbcPortID, + Order: ibc.Unordered, + Version: CUSTOM_IBC_VERSION, + }, ibc.CreateClientOptions{ + TrustingPeriod: "112h", + }) + require.NoError(t, err) + + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err) + err = r.StartRelayer(ctx, eRep, "wormosmo") + require.NoError(t, err) + + // Get the new wormchain channel to receive messages from the osmosis contract + wormholeChannelId := helpers.FindOpenChannelByVersion(t, ctx, eRep, r, wormchain, CUSTOM_IBC_VERSION).ChannelID + + // This is the channel we will send packets on from to wormhole from osmosis ibc contract + osmosisChannelId := helpers.FindOpenChannelByVersion(t, ctx, eRep, r, osmosis, CUSTOM_IBC_VERSION).ChannelID + + // Add the new channel to the wormchain-ibc-receiver contract + upgradeChainChannelVaa := wormchain_ibc_receiver.SubmitIbcReceiverUpdateChannelChainMsg(t, + vaa.ChainID(OsmoChainID), wormholeChannelId, + guardians) + _, err = wormchain.ExecuteContract(ctx, "faucet", wormchainReceiverContractInfo.Address, upgradeChainChannelVaa) + require.NoError(t, err) + + // Add the new channel to the osmosis wormhole-ibc contract + upgradeChainChannelVaa = wormhole_ibc.SubmitWormholeIbcUpdateChannelChainMsg(t, + vaa.ChainID(vaa.ChainIDWormchain), osmosisChannelId, + guardians) + _, err = osmosis.ExecuteContract(ctx, "faucet", osmosisSenderContractInfo.Address, upgradeChainChannelVaa) + require.NoError(t, err) + + // Send a VAA from osmosis to wormhole + postMessage := wormhole_ibc.ExecuteMsg{ + SubmitVAA: nil, + PostMessage: &wormhole_ibc.ExecuteMsg_PostMessage{ + Message: wormhole_ibc.Binary(base64.StdEncoding.EncodeToString([]byte("080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d9437338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d5b19866654c6980000000b150000000500020001020304000000000000000000000000000000000000000000000000000000000000000000000a0261626364"))), + Nonce: 0, + }, + SubmitUpdateChannelChain: nil, + } + postMessageJson, err := json.Marshal(postMessage) + require.NoError(t, err) + + postMessageTxHash, err := osmosis.ExecuteContract(ctx, "faucet", osmosisSenderContractInfo.Address, + string(postMessageJson)) + require.NoError(t, err, "failed to execute wormhole-ibc post message") + + ibcTx, err := helpers.GetIBCTx(osmosis, postMessageTxHash) + require.NoError(t, err, "failed to get ibc tx") + + // Poll for the receiver acknowledgement so that we can see if the packet was processed successfully + osmosisAck, err := testutil.PollForAck(ctx, osmosis, ibcTx.Height, ibcTx.Height+10, ibcTx.Packet) + require.NoError(t, err, "failed to poll for acknowledgement") + + var parsedAck wormchain_ibc_receiver.ReceiverAck + err = json.Unmarshal(osmosisAck.Acknowledgement, &parsedAck) + require.NoError(t, err, "failed to unmarshal acknowledgement") + + require.True(t, parsedAck.IsOk(), "receiver acknowledgement should be ok to signify that it was processed successfully") +} + +func TestIbcReceiverWithoutReceiverWhitelist(t *testing.T) { + // Base setup + numVals := 2 + guardians := guardians.CreateValSet(t, numVals) + chains := createChains(t, "v2.24.2", *guardians) + ctx, r, eRep, _ := buildInterchain(t, chains) + + // Chains + wormchain := chains[0].(*cosmos.CosmosChain) + osmosis := chains[1].(*cosmos.CosmosChain) + + // Instantiate the wormchain-ibc-receiver and wormhole-ibc contracts + wormchainReceiverContractInfo, osmosisSenderContractInfo := instantiateWormholeIbcContracts(t, ctx, wormchain, osmosis, guardians) + + // Spin up a new channel for the contracts to communicate over (this new channel will need to be whitelisted on the wormhole-ibc contract) + err := r.LinkPath(ctx, eRep, "wormosmo", ibc.CreateChannelOptions{ + SourcePortName: osmosisSenderContractInfo.ContractInfo.IbcPortID, + DestPortName: wormchainReceiverContractInfo.ContractInfo.IbcPortID, + Order: ibc.Unordered, + Version: CUSTOM_IBC_VERSION, + }, ibc.CreateClientOptions{ + TrustingPeriod: "112h", + }) + require.NoError(t, err) + + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err) + err = r.StartRelayer(ctx, eRep, "wormosmo") + require.NoError(t, err) + + // This is the channel we will send packets on from Osmosis to wormhole from the osmosis ibc contract + osmosisChannelId := helpers.FindOpenChannelByVersion(t, ctx, eRep, r, osmosis, CUSTOM_IBC_VERSION).ChannelID + + // SKIP UPGRADING THE WORMCHAIN IBC RECEIVER CONTRACT TO TEST THAT THE POST MESSAGE STILL COMPLETES + + // Add the new channel to the osmosis wormhole-ibc contract + upgradeChainChannelVaa := wormhole_ibc.SubmitWormholeIbcUpdateChannelChainMsg(t, + vaa.ChainID(vaa.ChainIDWormchain), osmosisChannelId, + guardians) + _, err = osmosis.ExecuteContract(ctx, "faucet", osmosisSenderContractInfo.Address, upgradeChainChannelVaa) + require.NoError(t, err) + + // Send a VAA from osmosis to wormhole + postMessage := wormhole_ibc.ExecuteMsg{ + SubmitVAA: nil, + PostMessage: &wormhole_ibc.ExecuteMsg_PostMessage{ + Message: wormhole_ibc.Binary(base64.StdEncoding.EncodeToString([]byte("080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d9437338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d5b19866654c6980000000b150000000500020001020304000000000000000000000000000000000000000000000000000000000000000000000a0261626364"))), + Nonce: 0, + }, + SubmitUpdateChannelChain: nil, + } + postMessageJson, err := json.Marshal(postMessage) + require.NoError(t, err) + + postMessageTxHash, err := osmosis.ExecuteContract(ctx, "faucet", osmosisSenderContractInfo.Address, + string(postMessageJson)) + require.NoError(t, err) + + ibcTx, err := helpers.GetIBCTx(osmosis, postMessageTxHash) + require.NoError(t, err) + + // Poll for the receiver acknowledgement so that we can see if the packet was processed successfully + osmosisAck, err := testutil.PollForAck(ctx, osmosis, ibcTx.Height, ibcTx.Height+10, ibcTx.Packet) + require.NoError(t, err) + + var parsedAck wormchain_ibc_receiver.ReceiverAck + err = json.Unmarshal(osmosisAck.Acknowledgement, &parsedAck) + require.NoError(t, err) + + require.True(t, parsedAck.IsOk(), "receiver acknowledgement should be ok to signify that it was processed successfully") +} + +func TestIbcReceiverWormholeIbcState(t *testing.T) { + // Base setup + numVals := 2 + guardians := guardians.CreateValSet(t, numVals) + chains := createChains(t, "v2.24.2", *guardians) + ctx, r, eRep, _ := buildInterchain(t, chains) + + // Chains + wormchain := chains[0].(*cosmos.CosmosChain) + osmosis := chains[1].(*cosmos.CosmosChain) + + // Instantiate the wormchain-ibc-receiver and wormhole-ibc contracts + wormchainReceiverContractInfo, osmosisSenderContractInfo := instantiateWormholeIbcContracts(t, ctx, wormchain, osmosis, guardians) + + // Spin up a new channel for the contracts to communicate over (this new channel will need to be whitelisted on the wormhole-ibc contract) + err := r.LinkPath(ctx, eRep, "wormosmo", ibc.CreateChannelOptions{ + SourcePortName: osmosisSenderContractInfo.ContractInfo.IbcPortID, + DestPortName: wormchainReceiverContractInfo.ContractInfo.IbcPortID, + Order: ibc.Unordered, + Version: CUSTOM_IBC_VERSION, + }, ibc.CreateClientOptions{ + TrustingPeriod: "112h", + }) + require.NoError(t, err) + + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err) + err = r.StartRelayer(ctx, eRep, "wormosmo") + require.NoError(t, err) + + // Get the new wormchain channel to receive messages from the osmosis contract + wormholeChannelId := helpers.FindOpenChannelByVersion(t, ctx, eRep, r, wormchain, CUSTOM_IBC_VERSION).ChannelID + + // This is the channel we will send packets on from to wormhole from osmosis ibc contract + _ = helpers.FindOpenChannelByVersion(t, ctx, eRep, r, osmosis, CUSTOM_IBC_VERSION).ChannelID + + // Add the new channel to the wormchain-ibc-receiver contract + upgradeChainChannelVaa := wormchain_ibc_receiver.SubmitIbcReceiverUpdateChannelChainMsg(t, + vaa.ChainID(OsmoChainID), wormholeChannelId, + guardians) + _, err = wormchain.ExecuteContract(ctx, "faucet", wormchainReceiverContractInfo.Address, upgradeChainChannelVaa) + require.NoError(t, err) + + // SKIPPING ADDING THE NEW CHANNEL TO THE WORMHOLE-IBC CONTRACT TO TEST THAT THE POST MESSAGE WILL NOT BE SENT + + // Send a VAA from osmosis to wormhole + postMessage := wormhole_ibc.ExecuteMsg{ + SubmitVAA: nil, + PostMessage: &wormhole_ibc.ExecuteMsg_PostMessage{ + Message: wormhole_ibc.Binary(base64.StdEncoding.EncodeToString([]byte("080000000901007bfa71192f886ab6819fa4862e34b4d178962958d9b2e3d9437338c9e5fde1443b809d2886eaa69e0f0158ea517675d96243c9209c3fe1d94d5b19866654c6980000000b150000000500020001020304000000000000000000000000000000000000000000000000000000000000000000000a0261626364"))), + Nonce: 0, + }, + SubmitUpdateChannelChain: nil, + } + postMessageJson, err := json.Marshal(postMessage) + require.NoError(t, err) + + _, err = osmosis.ExecuteContract(ctx, "faucet", osmosisSenderContractInfo.Address, + string(postMessageJson)) + require.Error(t, err, "post message should fail since the wormhole-ibc contract does not have the new channel whitelisted") +} + +func instantiateWormholeIbcContracts(t *testing.T, ctx context.Context, + wormchain *cosmos.CosmosChain, + remoteChain *cosmos.CosmosChain, + guardians *guardians.ValSet) (helpers.ContractInfoResponse, helpers.ContractInfoResponse) { + + // Instantiate the Wormchain core contract + coreInstantiateMsg := helpers.CoreContractInstantiateMsg(t, wormchainConfig, guardians) + wormchainCoreContractInfo := helpers.StoreAndInstantiateWormholeContract(t, ctx, wormchain, "faucet", "./contracts/wormhole_core.wasm", "wormhole_core", coreInstantiateMsg, guardians) + + // Store wormhole-ibc-receiver contract on wormchain + ibcReceiverContractCodeId := helpers.StoreContract(t, ctx, wormchain, "faucet", "./contracts/wormchain_ibc_receiver.wasm", guardians) + ibcReceiverCodeId, err := strconv.ParseUint(ibcReceiverContractCodeId, 10, 32) + require.NoError(t, err) + + // Migrate the core wormchain core contract to the ibc variant + helpers.MigrateContract(t, ctx, wormchain, "faucet", wormchainCoreContractInfo.Address, fmt.Sprint(ibcReceiverCodeId), "{}", guardians) + + // Get the port id for the wormchain-ibc-receiver contract + wormchainReceiverContractInfo := helpers.QueryContractInfo(t, wormchain, ctx, wormchainCoreContractInfo.Address) + require.NotEmpty(t, wormchainReceiverContractInfo.ContractInfo.IbcPortID, "wormchain (wormchain-ibc-receiver) contract port id is nil") + + // Store and instantiate wormhole-ibc contract on osmosis + senderInstantiateMsg := helpers.CoreContractInstantiateMsg(t, wormchainConfig, guardians) + senderCodeId, err := remoteChain.StoreContract(ctx, "faucet", "./contracts/wormhole_ibc.wasm") + require.NoError(t, err) + senderContractAddr, err := remoteChain.InstantiateContract(ctx, "faucet", senderCodeId, senderInstantiateMsg, true) + require.NoError(t, err) + senderContractInfo := helpers.QueryContractInfo(t, remoteChain, ctx, senderContractAddr) + require.NotEmpty(t, senderContractInfo.ContractInfo.IbcPortID, "sender (wormhole-ibc) contract port id is nil") + + return wormchainReceiverContractInfo, senderContractInfo +}