diff --git a/crates/core/app/tests/common/ibc_tests/node.rs b/crates/core/app/tests/common/ibc_tests/node.rs index 5026bd7c2a..5178c7dd3f 100644 --- a/crates/core/app/tests/common/ibc_tests/node.rs +++ b/crates/core/app/tests/common/ibc_tests/node.rs @@ -10,8 +10,11 @@ use { }, ibc_types::{ core::{ + channel::{ChannelEnd, ChannelId, PortId, Version as ChannelVersion}, client::{ClientId, ClientType, Height}, - connection::{ChainId, ConnectionEnd, ConnectionId, Counterparty, Version}, + connection::{ + ChainId, ConnectionEnd, ConnectionId, Counterparty, Version as ConnectionVersion, + }, }, lightclients::tendermint::{ consensus_state::ConsensusState, header::Header as TendermintHeader, @@ -46,17 +49,21 @@ use { #[allow(unused)] pub struct TestNodeWithIBC { pub connection_id: ConnectionId, + pub channel_id: ChannelId, pub client_id: ClientId, + pub port_id: PortId, pub chain_id: String, pub counterparty: Counterparty, - pub version: Version, + pub connection_version: ConnectionVersion, + pub channel_version: ChannelVersion, pub signer: String, pub connection: Option, + pub channel: Option, pub node: TestNode>>, pub storage: TempStorage, pub ibc_client_query_client: IbcClientQueryClient, pub ibc_connection_query_client: IbcConnectionQueryClient, - pub _ibc_channel_query_client: IbcChannelQueryClient, + pub ibc_channel_query_client: IbcChannelQueryClient, pub tendermint_proxy_service_client: TendermintProxyServiceClient, } @@ -151,6 +158,10 @@ impl TestNodeWithIBC { Ok(Self { // the test relayer supports only a single connection on each chain as of now connection_id: ConnectionId::new(0), + // the test relayer supports only a single channel per connection on each chain as of now + channel_id: ChannelId::new(0), + // Only ICS20 transfers are supported + port_id: PortId::transfer(), node, storage, client_id: ClientId::new(ClientType::new("07-tendermint".to_string()), 0)?, @@ -160,11 +171,13 @@ impl TestNodeWithIBC { connection_id: None, prefix: IBC_COMMITMENT_PREFIX.to_owned(), }, - version: Version::default(), + connection_version: ConnectionVersion::default(), + channel_version: ChannelVersion::new("ics20-1".to_string()), signer: hex::encode_upper(proposer_address), connection: None, + channel: None, ibc_connection_query_client, - _ibc_channel_query_client: ibc_channel_query_client, + ibc_channel_query_client, ibc_client_query_client, tendermint_proxy_service_client, }) diff --git a/crates/core/app/tests/common/ibc_tests/relayer.rs b/crates/core/app/tests/common/ibc_tests/relayer.rs index 86705d770d..05a73e1f59 100644 --- a/crates/core/app/tests/common/ibc_tests/relayer.rs +++ b/crates/core/app/tests/common/ibc_tests/relayer.rs @@ -2,12 +2,19 @@ use { super::TestNodeWithIBC, anyhow::{anyhow, Result}, ibc_proto::ibc::core::{ + channel::v1::{IdentifiedChannel, QueryChannelRequest, QueryConnectionChannelsRequest}, client::v1::{QueryClientStateRequest, QueryConsensusStateRequest}, connection::v1::QueryConnectionRequest, }, - ibc_types::lightclients::tendermint::client_state::ClientState as TendermintClientState, ibc_types::{ core::{ + channel::{ + channel::{Order, State as ChannelState}, + msgs::{ + MsgChannelOpenAck, MsgChannelOpenConfirm, MsgChannelOpenInit, MsgChannelOpenTry, + }, + IdentifiedChannelEnd, Version as ChannelVersion, + }, client::{ msgs::{MsgCreateClient, MsgUpdateClient}, Height, @@ -18,22 +25,27 @@ use { MsgConnectionOpenAck, MsgConnectionOpenConfirm, MsgConnectionOpenInit, MsgConnectionOpenTry, }, - ConnectionEnd, Counterparty, State as ConnectionState, Version, + ConnectionEnd, Counterparty, State as ConnectionState, + Version as ConnectionVersion, }, }, lightclients::tendermint::{ - client_state::AllowUpdate, consensus_state::ConsensusState, - header::Header as TendermintHeader, TrustThreshold, + client_state::{AllowUpdate, ClientState as TendermintClientState}, + consensus_state::ConsensusState, + header::Header as TendermintHeader, + TrustThreshold, }, DomainType as _, }, penumbra_ibc::{ - component::ConnectionStateReadExt as _, IbcRelay, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, + component::{ChannelStateReadExt as _, ConnectionStateReadExt as _}, + IbcRelay, IBC_COMMITMENT_PREFIX, IBC_PROOF_SPECS, }, penumbra_proto::{util::tendermint_proxy::v1::GetBlockByHeightRequest, DomainType}, penumbra_stake::state_key::chain, penumbra_transaction::{TransactionParameters, TransactionPlan}, prost::Message as _, + rand_chacha::ChaCha12Core, sha2::Digest, std::time::Duration, tendermint::Time, @@ -90,7 +102,64 @@ impl MockRelayer { ) } + pub async fn get_channel_states(&mut self) -> Result<(ChannelState, ChannelState)> { + let channel_on_a_response = self + .chain_a_ibc + .ibc_channel_query_client + .connection_channels(QueryConnectionChannelsRequest { + connection: self.chain_a_ibc.connection_id.to_string(), + pagination: None, + }) + .await? + .into_inner(); + let channel_on_b_response = self + .chain_b_ibc + .ibc_channel_query_client + .connection_channels(QueryConnectionChannelsRequest { + connection: self.chain_b_ibc.connection_id.to_string(), + pagination: None, + }) + .await? + .into_inner(); + + let channels_a = channel_on_a_response.channels; + let channels_b = channel_on_b_response.channels; + + // Note: Mock relayer expects only a single channel per connection right now + let channel_a_state = match channels_a.len() { + 0 => ChannelState::Uninitialized, + _ => { + let channel_a: IdentifiedChannelEnd = channels_a[0].clone().try_into().unwrap(); + channel_a.channel_end.state.try_into()? + } + }; + let channel_b_state = match channels_b.len() { + 0 => ChannelState::Uninitialized, + _ => { + let channel_b: IdentifiedChannelEnd = channels_b[0].clone().try_into().unwrap(); + channel_b.channel_end.state.try_into()? + } + }; + + Ok((channel_a_state, channel_b_state)) + } + + /// Performs a connection handshake followed by a channel handshake + /// between the two chains owned by the mock relayer. pub async fn _handshake(&mut self) -> Result<(), anyhow::Error> { + // Perform connection handshake + self._connection_handshake().await?; + + // Perform channel handshake + self._channel_handshake().await?; + + // The two chains should now be able to perform IBC transfers + // between each other. + Ok(()) + } + + /// Establish a connection between the two chains owned by the mock relayer. + pub async fn _connection_handshake(&mut self) -> Result<(), anyhow::Error> { // The IBC connection handshake has four steps (Init, Try, Ack, Confirm). // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L672 // https://github.com/cosmos/ibc/blob/main/spec/core/ics-003-connection-semantics/README.md#opening-handshake @@ -104,7 +173,7 @@ impl MockRelayer { // 1: send the Init message to chain A { - tracing::info!("Send Init to chain A"); + tracing::info!("Send Connection Init to chain A"); self._build_and_send_connection_open_init().await?; } @@ -115,7 +184,7 @@ impl MockRelayer { // 2. send the OpenTry message to chain B { - tracing::info!("send OpenTry to chain B"); + tracing::info!("send Connection OpenTry to chain B"); self._build_and_send_connection_open_try().await?; } @@ -126,7 +195,7 @@ impl MockRelayer { // 3. Send the OpenAck message to chain A { - tracing::info!("send OpenAck to chain A"); + tracing::info!("send Connection OpenAck to chain A"); self._build_and_send_connection_open_ack().await?; } @@ -137,7 +206,7 @@ impl MockRelayer { // 4. Send the OpenConfirm message to chain B { - tracing::info!("send OpenConfirm to chain B"); + tracing::info!("send Connection OpenConfirm to chain B"); self._build_and_send_connection_open_confirm().await?; } @@ -150,6 +219,65 @@ impl MockRelayer { Ok(()) } + /// Establish a channel between the two chains owned by the mock relayer. + pub async fn _channel_handshake(&mut self) -> Result<(), anyhow::Error> { + // The IBC channel handshake has four steps (Init, Try, Ack, Confirm). + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/channel.rs#L712 + // https://github.com/cosmos/ibc/blob/main/spec/core/ics-004-channel-and-packet-semantics/README.md + + self._sync_chains().await?; + + let (a_state, b_state) = self.get_channel_states().await?; + assert!(a_state == ChannelState::Uninitialized && b_state == ChannelState::Uninitialized); + + // 1: send the Init message to chain A + { + tracing::info!("Send Channel Init to chain A"); + self._build_and_send_channel_open_init().await?; + } + + let (a_state, b_state) = self.get_channel_states().await?; + assert!(a_state == ChannelState::Init && b_state == ChannelState::Uninitialized); + + self._sync_chains().await?; + + // 2. send the OpenTry message to chain B + { + tracing::info!("send Channel OpenTry to chain B"); + self._build_and_send_channel_open_try().await?; + } + + let (a_state, b_state) = self.get_channel_states().await?; + assert!(a_state == ChannelState::Init && b_state == ChannelState::TryOpen); + + self._sync_chains().await?; + + // 3. Send the OpenAck message to chain A + { + tracing::info!("send Channel OpenAck to chain A"); + self._build_and_send_channel_open_ack().await?; + } + + let (a_state, b_state) = self.get_channel_states().await?; + assert!(a_state == ChannelState::Open && b_state == ChannelState::TryOpen); + + self._sync_chains().await?; + + // 4. Send the OpenConfirm message to chain B + { + tracing::info!("send Channel OpenConfirm to chain B"); + self._build_and_send_channel_open_confirm().await?; + } + + let (a_state, b_state) = self.get_channel_states().await?; + assert!(a_state == ChannelState::Open && b_state == ChannelState::Open); + + // Ensure the chain timestamps remain in sync + self._sync_chains().await?; + + Ok(()) + } + pub async fn _create_clients(&mut self) -> Result<(), anyhow::Error> { self._sync_chains().await?; // helper function to create client for chain B on chain A @@ -259,7 +387,7 @@ impl MockRelayer { let ibc_msg = IbcRelay::ConnectionOpenInit(MsgConnectionOpenInit { client_id_on_a: chain_a_ibc.client_id.clone(), counterparty: chain_a_ibc.counterparty.clone(), - version: Some(chain_a_ibc.version.clone()), + version: Some(chain_a_ibc.connection_version.clone()), delay_period: Duration::from_secs(1), signer: chain_b_ibc.signer.clone(), }) @@ -318,6 +446,217 @@ impl MockRelayer { Ok(()) } + // helper function to build ChannelOpenTry to chain B + pub async fn _build_and_send_channel_open_try(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + let src_connection = self + .chain_a_ibc + .ibc_connection_query_client + .connection(QueryConnectionRequest { + connection_id: self.chain_a_ibc.connection_id.to_string(), + }) + .await? + .into_inner(); + + let chain_b_height = self._build_and_send_update_client_a().await?; + let chain_a_height = self._build_and_send_update_client_b().await?; + + let chan_end_on_a_response = self + .chain_a_ibc + .ibc_channel_query_client + .channel(QueryChannelRequest { + port_id: self.chain_a_ibc.port_id.to_string(), + channel_id: self.chain_a_ibc.channel_id.to_string(), + }) + .await? + .into_inner(); + + let proof_chan_end_on_a = + MerkleProof::decode(chan_end_on_a_response.clone().proof.as_slice())?; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + let proof_height_on_a: Height = chan_end_on_a_response + .proof_height + .clone() + .unwrap() + .try_into()?; + + self._build_and_send_update_client_b().await?; + self._sync_chains().await?; + + let plan = { + // This mocks the relayer constructing a channel open try message on behalf + // of the counterparty chain. + #[allow(deprecated)] + let ibc_msg = IbcRelay::ChannelOpenTry(MsgChannelOpenTry { + signer: self.chain_a_ibc.signer.clone(), + port_id_on_b: self.chain_b_ibc.port_id.clone(), + connection_hops_on_b: vec![self.chain_b_ibc.connection_id.clone()], + port_id_on_a: self.chain_a_ibc.port_id.clone(), + chan_id_on_a: self.chain_a_ibc.channel_id.clone(), + version_supported_on_a: self.chain_a_ibc.channel_version.clone(), + proof_chan_end_on_a, + proof_height_on_a, + // Ordering must be Unordered for ics20 transfer + ordering: Order::Unordered, + // Deprecated + previous_channel_id: self.chain_a_ibc.channel_id.to_string(), + // Deprecated: Only ics20 version is supported + version_proposal: ChannelVersion::new("ics20-1".to_string()), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_b_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the chain b pre-tx storage root hash is what we expect: + let pre_tx_hash = pre_tx_snapshot.root_hash().await?; + + // Validate the tx hash is what we expect: + let tx_hash = sha2::Sha256::digest(&tx.encode_to_vec()); + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + + // execute the transaction containing the opentry message + self.chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + self.chain_b_ibc.node.block().execute().await?; + let post_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the channel state is now "tryopen" + { + // Channel should not exist pre-commit + assert!(pre_tx_snapshot + .get_channel(&self.chain_b_ibc.channel_id, &self.chain_b_ibc.port_id) + .await? + .is_none(),); + + // Post-commit, the connection should be in the "tryopen" state. + let channel = post_tx_snapshot + .get_channel(&self.chain_b_ibc.channel_id, &self.chain_b_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channel with the specified ID {} exists", + &self.chain_b_ibc.channel_id + ) + })?; + + assert_eq!(channel.state, ChannelState::TryOpen); + + self.chain_b_ibc.channel = Some(channel); + } + + self._sync_chains().await?; + + Ok(()) + } + + // helper function to build ChannelOpenInit to chain A + pub async fn _build_and_send_channel_open_init(&mut self) -> Result<()> { + self._sync_chains().await?; + let chain_a_ibc = &mut self.chain_a_ibc; + let chain_b_ibc = &mut self.chain_b_ibc; + + let plan = { + let ibc_msg = IbcRelay::ChannelOpenInit(MsgChannelOpenInit { + signer: chain_b_ibc.signer.clone(), + port_id_on_a: chain_a_ibc.port_id.clone(), + connection_hops_on_a: vec![chain_b_ibc + .counterparty + .connection_id + .clone() + .expect("connection established")], + port_id_on_b: chain_b_ibc.port_id.clone(), + // ORdering must be unordered for Ics20 transfer + ordering: Order::Unordered, + // Only ics20 version is supported + version_proposal: ChannelVersion::new("ics20-1".to_string()), + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = chain_a_ibc.storage.latest_snapshot(); + chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = chain_a_ibc.storage.latest_snapshot(); + + // validate the connection state is now "init" + { + // Channel should not exist pre-commit + assert!(pre_tx_snapshot + .get_channel(&chain_a_ibc.channel_id, &chain_a_ibc.port_id) + .await? + .is_none(),); + + // Post-commit, the channel should be in the "init" state. + let channel = post_tx_snapshot + .get_channel(&chain_a_ibc.channel_id, &chain_a_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channel with the specified ID {} exists", + &chain_a_ibc.channel_id + ) + })?; + + assert_eq!(channel.state.clone(), ChannelState::Init); + + chain_a_ibc.channel = Some(channel.clone()); + } + + Ok(()) + } + pub async fn handshake(&mut self) -> Result<(), anyhow::Error> { // Open a connection on each chain to the other chain. // This is accomplished by following the ICS-003 spec for connection handshakes. @@ -378,6 +717,120 @@ impl MockRelayer { _build_and_send_update_client(chain_a_ibc, chain_b_ibc).await } + // Send an ACK message to chain A + pub async fn _build_and_send_channel_open_ack(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + let chain_b_connection_id = self.chain_b_ibc.connection_id.clone(); + let chain_a_connection_id = self.chain_a_ibc.connection_id.clone(); + + // Build message(s) for updating client on source + let src_client_height = self._build_and_send_update_client_a().await?; + // Build message(s) for updating client on destination + let dst_client_height = self._build_and_send_update_client_b().await?; + + let chan_end_on_b_response = self + .chain_b_ibc + .ibc_channel_query_client + .channel(QueryChannelRequest { + port_id: self.chain_b_ibc.port_id.to_string(), + channel_id: self.chain_b_ibc.channel_id.to_string(), + }) + .await? + .into_inner(); + + let proof_height_on_b = chan_end_on_b_response + .clone() + .proof_height + .expect("proof height should be present") + .try_into()?; + let proof_chan_end_on_b = + MerkleProof::decode(chan_end_on_b_response.clone().proof.as_slice())?; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._build_and_send_update_client_a().await?; + self._sync_chains().await?; + + let plan = { + // This mocks the relayer constructing a channel open try message on behalf + // of the counterparty chain. + let ibc_msg = IbcRelay::ChannelOpenAck(MsgChannelOpenAck { + port_id_on_a: self.chain_a_ibc.port_id.clone(), + chan_id_on_a: self.chain_a_ibc.channel_id.clone(), + chan_id_on_b: self.chain_b_ibc.channel_id.clone(), + version_on_b: self.chain_b_ibc.channel_version.clone(), + proof_chan_end_on_b, + proof_height_on_b, + signer: self.chain_b_ibc.signer.clone(), + }) + .into(); + // let ibc_msg = IbcRelay::ChannelOpenAck(MsgChannelOpenAck::try_from(proto_ack)?).into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_a_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_a_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_a_ibc.storage.latest_snapshot(); + self.chain_a_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = self.chain_a_ibc.storage.latest_snapshot(); + + // validate the channel state is now "OPEN" + { + // Channel should be in INIT pre-commit + let channel = pre_tx_snapshot + .get_channel(&self.chain_a_ibc.channel_id, &self.chain_a_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channel with the specified ID {} exists", + &self.chain_a_ibc.channel_id + ) + })?; + + assert_eq!(channel.state, ChannelState::Init); + + // Post-commit, the channel should be in the "OPEN" state. + let channel = post_tx_snapshot + .get_channel(&self.chain_a_ibc.channel_id, &self.chain_a_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channelwith the specified ID {} exists", + &self.chain_a_ibc.channel_id + ) + })?; + + assert_eq!(channel.state, ChannelState::Open); + + self.chain_a_ibc.channel = Some(channel); + } + + Ok(()) + } + // Send an ACK message to chain A // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1126 pub async fn _build_and_send_connection_open_ack(&mut self) -> Result<()> { @@ -445,7 +898,7 @@ impl MockRelayer { let proto_ack = ibc_proto::ibc::core::connection::v1::MsgConnectionOpenAck { connection_id: self.chain_a_ibc.connection_id.to_string(), counterparty_connection_id: chain_b_connection_id.to_string(), - version: Some(Version::default().into()), + version: Some(ConnectionVersion::default().into()), client_state: Some( client_state_of_a_on_b_response .clone() @@ -643,7 +1096,7 @@ impl MockRelayer { client_state_of_b_on_a: client_state_of_b_on_a_response .client_state .expect("client state present"), - versions_on_a: vec![Version::default()], + versions_on_a: vec![ConnectionVersion::default()], proof_conn_end_on_a, proof_client_state_of_b_on_a, proof_consensus_state_of_b_on_a, @@ -827,6 +1280,106 @@ impl MockRelayer { Ok(()) } + + // sends a ChannelOpenConfirm message to chain B + // at this point, chain A is in OPEN and B is in TRYOPEN. + // afterwards, chain A will be in OPEN and chain B will be in OPEN. + pub async fn _build_and_send_channel_open_confirm(&mut self) -> Result<()> { + // This is a load-bearing block execution that should be removed + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._sync_chains().await?; + + // https://github.com/penumbra-zone/hermes/blob/a34a11fec76de3b573b539c237927e79cb74ec00/crates/relayer/src/connection.rs#L1296 + let chan_end_on_a_response = self + .chain_a_ibc + .ibc_channel_query_client + .channel(QueryChannelRequest { + port_id: self.chain_a_ibc.port_id.to_string(), + channel_id: self.chain_a_ibc.channel_id.to_string(), + }) + .await? + .into_inner(); + + let dst_client_target_height = self._build_and_send_update_client_b().await?; + + self.chain_a_ibc.node.block().execute().await?; + self.chain_b_ibc.node.block().execute().await?; + self._build_and_send_update_client_b().await?; + self._sync_chains().await?; + + let plan = { + // This mocks the relayer constructing a channel open confirm message on behalf + // of the counterparty chain. + let ibc_msg = IbcRelay::ChannelOpenConfirm(MsgChannelOpenConfirm { + proof_height_on_a: chan_end_on_a_response.proof_height.unwrap().try_into()?, + signer: self.chain_a_ibc.signer.clone(), + port_id_on_b: self.chain_b_ibc.port_id.clone(), + chan_id_on_b: self.chain_b_ibc.channel_id.clone(), + proof_chan_end_on_a: MerkleProof::decode(chan_end_on_a_response.proof.as_slice())?, + }) + .into(); + TransactionPlan { + actions: vec![ibc_msg], + // Now fill out the remaining parts of the transaction needed for verification: + memo: None, + detection_data: None, // We'll set this automatically below + transaction_parameters: TransactionParameters { + chain_id: self.chain_b_ibc.chain_id.clone(), + ..Default::default() + }, + } + }; + let tx = self + .chain_b_ibc + .client() + .await? + .witness_auth_build(&plan) + .await?; + + // Execute the transaction, applying it to the chain state. + let pre_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + self.chain_b_ibc + .node + .block() + .with_data(vec![tx.encode_to_vec()]) + .execute() + .await?; + let post_tx_snapshot = self.chain_b_ibc.storage.latest_snapshot(); + + // validate the channel state is now "open" + { + // Channel should be in TRYOPEN pre-commit + let channel = pre_tx_snapshot + .get_channel(&self.chain_b_ibc.channel_id, &self.chain_b_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channel with the specified ID {} exists", + &self.chain_b_ibc.channel_id + ) + })?; + + assert_eq!(channel.state, ChannelState::TryOpen); + + // Post-commit, the channel should be in the "OPEN" state. + let channel = post_tx_snapshot + .get_channel(&self.chain_b_ibc.channel_id, &self.chain_b_ibc.port_id) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "no channel with the specified ID {} exists", + &self.chain_b_ibc.channel_id + ) + })?; + + assert_eq!(channel.state, ChannelState::Open); + + self.chain_b_ibc.channel = Some(channel); + } + + Ok(()) + } } // tell chain A about chain B. returns the height of chain b on chain a after update. diff --git a/crates/core/app/tests/ibc_handshake.rs b/crates/core/app/tests/ibc_handshake.rs index e000a824eb..2068d9d6b8 100644 --- a/crates/core/app/tests/ibc_handshake.rs +++ b/crates/core/app/tests/ibc_handshake.rs @@ -69,7 +69,7 @@ async fn ibc_handshake() -> anyhow::Result<()> { chain_b_ibc, }; - // Perform the IBC connection handshake between the two chains. + // Perform the IBC connection and channel handshakes between the two chains. // TODO: some testing of failure cases of the handshake process would be good relayer.handshake().await?;