diff --git a/Cargo.lock b/Cargo.lock index 5ad772a2..78ad4b5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3858,6 +3858,7 @@ dependencies = [ "sp1-ics07-tendermint-utils", "sp1-sdk", "tendermint", + "tendermint-light-client-verifier", "tendermint-rpc", "tracing", ] diff --git a/packages/relayer-lib/Cargo.toml b/packages/relayer-lib/Cargo.toml index 33c15af1..4429f830 100644 --- a/packages/relayer-lib/Cargo.toml +++ b/packages/relayer-lib/Cargo.toml @@ -17,7 +17,7 @@ sp1-ics07-tendermint-prover = { workspace = true, optional = true } sp1-ics07-tendermint-utils = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } -prost = { workspace = true } +prost = { workspace = true, features = ["std"] } async-trait = { workspace = true } anyhow = { workspace = true, features = ["std"] } @@ -26,6 +26,7 @@ tracing = { workspace = true, default-features = true } tendermint = { workspace = true, features = ["std"] } tendermint-rpc = { workspace = true, features = ["http-client"] } +tendermint-light-client-verifier = { workspace = true } ibc-proto-eureka = { workspace = true } ibc-core-host-types = { workspace = true } diff --git a/packages/relayer-lib/src/events/eureka.rs b/packages/relayer-lib/src/events/eureka.rs index 55e97360..09f9c70a 100644 --- a/packages/relayer-lib/src/events/eureka.rs +++ b/packages/relayer-lib/src/events/eureka.rs @@ -2,7 +2,7 @@ use alloy::{hex, sol_types::SolEvent}; use ibc_eureka_solidity_types::ics26::router::{ - routerEvents, AckPacket, RecvPacket, SendPacket, TimeoutPacket, WriteAcknowledgement, + routerEvents, RecvPacket, SendPacket, WriteAcknowledgement, }; use ibc_proto_eureka::ibc::core::channel::v2::{Acknowledgement, Packet}; use prost::Message; @@ -18,10 +18,6 @@ pub enum EurekaEvent { SendPacket(SendPacket), /// A packet was received. RecvPacket(RecvPacket), - /// A packet was acknowledged. - AckPacket(AckPacket), - /// A packet timed out. - TimeoutPacket(TimeoutPacket), /// An acknowledgement was written. WriteAcknowledgement(WriteAcknowledgement), } @@ -30,12 +26,10 @@ impl EurekaEvent { /// Get the signature of the events for EVM. /// This is used to filter the logs. #[must_use] - pub const fn evm_signatures() -> [&'static str; 5] { + pub const fn evm_signatures() -> [&'static str; 3] { [ SendPacket::SIGNATURE, RecvPacket::SIGNATURE, - AckPacket::SIGNATURE, - TimeoutPacket::SIGNATURE, WriteAcknowledgement::SIGNATURE, ] } @@ -48,9 +42,11 @@ impl TryFrom for EurekaEvent { match event { routerEvents::SendPacket(event) => Ok(Self::SendPacket(event)), routerEvents::RecvPacket(event) => Ok(Self::RecvPacket(event)), - routerEvents::AckPacket(event) => Ok(Self::AckPacket(event)), - routerEvents::TimeoutPacket(event) => Ok(Self::TimeoutPacket(event)), routerEvents::WriteAcknowledgement(event) => Ok(Self::WriteAcknowledgement(event)), + routerEvents::AckPacket(_) => Err(anyhow::anyhow!("AckPacket event is not used")), + routerEvents::TimeoutPacket(_) => { + Err(anyhow::anyhow!("TimeoutPacket event is not used")) + } routerEvents::Noop(_) => Err(anyhow::anyhow!("Noop event")), routerEvents::IBCAppAdded(_) => Err(anyhow::anyhow!("IBCAppAdded event")), routerEvents::OwnershipTransferred(_) => { @@ -69,30 +65,28 @@ impl TryFrom for EurekaEvent { .attributes .into_iter() .find_map(|attr| { - if attr.key_str().ok()? == cosmos_sdk::ATTRIBUTE_KEY_ENCODED_PACKET_HEX { - let packet: Vec = hex::decode(attr.value_str().ok()?).ok()?; - let packet = Packet::decode(packet.as_slice()).ok()?; - Some(Self::SendPacket(SendPacket { - packet: packet.try_into().ok()?, - })) - } else { - None + if attr.key_str().ok()? != cosmos_sdk::ATTRIBUTE_KEY_ENCODED_PACKET_HEX { + return None; } + let packet: Vec = hex::decode(attr.value_str().ok()?).ok()?; + let packet = Packet::decode(packet.as_slice()).ok()?; + Some(Self::SendPacket(SendPacket { + packet: packet.try_into().ok()?, + })) }) .ok_or_else(|| anyhow::anyhow!("No packet data found")), cosmos_sdk::EVENT_TYPE_RECV_PACKET => event .attributes .into_iter() .find_map(|attr| { - if attr.key_str().ok()? == cosmos_sdk::ATTRIBUTE_KEY_ENCODED_PACKET_HEX { - let packet: Vec = hex::decode(attr.value_str().ok()?).ok()?; - let packet = Packet::decode(packet.as_slice()).ok()?; - Some(Self::RecvPacket(RecvPacket { - packet: packet.try_into().ok()?, - })) - } else { - None + if attr.key_str().ok()? != cosmos_sdk::ATTRIBUTE_KEY_ENCODED_PACKET_HEX { + return None; } + let packet: Vec = hex::decode(attr.value_str().ok()?).ok()?; + let packet = Packet::decode(packet.as_slice()).ok()?; + Some(Self::RecvPacket(RecvPacket { + packet: packet.try_into().ok()?, + })) }) .ok_or_else(|| anyhow::anyhow!("No packet data found")), cosmos_sdk::EVENT_TYPE_WRITE_ACK => { diff --git a/packages/relayer-lib/src/lib.rs b/packages/relayer-lib/src/lib.rs index 0b192280..d0f9fdba 100644 --- a/packages/relayer-lib/src/lib.rs +++ b/packages/relayer-lib/src/lib.rs @@ -6,3 +6,4 @@ pub mod chain; pub mod events; pub mod listener; pub mod tx_builder; +pub(crate) mod utils; diff --git a/packages/relayer-lib/src/tx_builder/cosmos_to_cosmos.rs b/packages/relayer-lib/src/tx_builder/cosmos_to_cosmos.rs index c2ac5336..9372af0c 100644 --- a/packages/relayer-lib/src/tx_builder/cosmos_to_cosmos.rs +++ b/packages/relayer-lib/src/tx_builder/cosmos_to_cosmos.rs @@ -2,24 +2,26 @@ //! the Cosmos SDK chain from events received from another Cosmos SDK chain. use anyhow::Result; -use futures::future; use ibc_proto_eureka::{ + cosmos::tx::v1beta1::TxBody, + google::protobuf::Any, ibc::{ core::{ - channel::v2::{ - Channel, MsgRecvPacket, MsgTimeout, QueryChannelRequest, QueryChannelResponse, - }, - client::v1::{Height, MsgUpdateClient}, + channel::v2::{Channel, QueryChannelRequest, QueryChannelResponse}, + client::v1::MsgUpdateClient, }, lightclients::tendermint::v1::ClientState, }, - Protobuf, }; use prost::Message; use sp1_ics07_tendermint_utils::{light_block::LightBlockExt, rpc::TendermintRpcExt}; use tendermint_rpc::{Client, HttpClient}; -use crate::{chain::CosmosSdk, events::EurekaEvent}; +use crate::{ + chain::CosmosSdk, + events::EurekaEvent, + utils::cosmos::{src_events_to_recv_and_ack_msgs, target_events_to_timeout_msgs}, +}; use super::r#trait::TxBuilderService; @@ -71,17 +73,12 @@ impl TxBuilder { #[async_trait::async_trait] impl TxBuilderService for TxBuilder { - #[allow(clippy::too_many_lines)] async fn relay_events( &self, src_events: Vec, target_events: Vec, target_channel_id: String, ) -> Result> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs(); - let channel = self.channel(target_channel_id.clone()).await?; let client_state = ClientState::decode( self.target_tm_client @@ -93,132 +90,30 @@ impl TxBuilderService for TxBuilder { let target_light_block = self.source_tm_client.get_light_block(None).await?; let target_height = target_light_block.height().value().try_into()?; - - let _timeout_msgs = future::try_join_all( - target_events - .into_iter() - .filter(|e| match e { - EurekaEvent::SendPacket(se) => { - now >= se.packet.timeoutTimestamp - && se.packet.sourceChannel == target_channel_id - } - _ => false, - }) - .map(|e| async { - match e { - EurekaEvent::SendPacket(se) => { - let ibc_path = se.packet.receipt_commitment_path(); - self.source_tm_client - .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) - .await - .map(|(v, p)| { - if v.is_empty() { - Some(MsgTimeout { - packet: Some(se.packet.into()), - proof_unreceived: p.encode_vec(), - proof_height: Some(Height { - revision_number: client_state - .latest_height - .unwrap_or_default() - .revision_number, - revision_height: target_height.into(), - }), - signer: self.signer_address.clone(), - }) - } else { - None - } - }) - } - _ => unreachable!(), - } - }), + let revision_number = client_state + .latest_height + .ok_or_else(|| anyhow::anyhow!("No latest height found"))? + .revision_number; + + let timeout_msgs = target_events_to_timeout_msgs( + target_events, + &self.source_tm_client, + &target_channel_id, + revision_number, + target_height, + &self.signer_address, ) - .await? - .into_iter() - .flatten() - .collect::>(); - - let (src_send_events, src_ack_events): (Vec<_>, Vec<_>) = src_events - .into_iter() - .filter(|e| match e { - EurekaEvent::SendPacket(se) => { - se.packet.timeoutTimestamp > now && se.packet.destChannel == target_channel_id - } - EurekaEvent::WriteAcknowledgement(we) => { - we.packet.sourceChannel == target_channel_id - } - _ => false, - }) - .partition(|e| match e { - EurekaEvent::SendPacket(_) => true, - EurekaEvent::WriteAcknowledgement(_) => false, - _ => unreachable!(), - }); - - let _recv_msgs = future::try_join_all(src_send_events.into_iter().map(|e| async { - match e { - EurekaEvent::SendPacket(se) => { - let ibc_path = se.packet.commitment_path(); - self.source_tm_client - .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) - .await - .map(|(v, p)| { - if v.is_empty() { - Some(MsgRecvPacket { - packet: Some(se.packet.into()), - proof_height: Some(Height { - revision_number: client_state - .latest_height - .unwrap_or_default() - .revision_number, - revision_height: target_height.into(), - }), - proof_commitment: p.encode_vec(), - signer: self.signer_address.clone(), - }) - } else { - None - } - }) - } - _ => unreachable!(), - } - })) - .await? - .into_iter() - .flatten() - .collect::>(); - - let _ack_msgs = future::try_join_all(src_ack_events.into_iter().map(|e| async { - match e { - EurekaEvent::WriteAcknowledgement(we) => { - let ibc_path = we.packet.ack_commitment_path(); - self.source_tm_client - .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) - .await - .map(|(v, p)| { - if v.is_empty() { - Some(MsgRecvPacket { - packet: Some(we.packet.into()), - proof_height: Some(Height { - revision_number: client_state - .latest_height - .unwrap_or_default() - .revision_number, - revision_height: target_height.into(), - }), - proof_commitment: p.encode_vec(), - signer: self.signer_address.clone(), - }) - } else { - None - } - }) - } - _ => unreachable!(), - } - })); + .await?; + + let (recv_msgs, ack_msgs) = src_events_to_recv_and_ack_msgs( + src_events, + &self.source_tm_client, + &target_channel_id, + revision_number, + target_height, + &self.signer_address, + ) + .await?; let trusted_light_block = self .source_tm_client @@ -230,15 +125,23 @@ impl TxBuilderService for TxBuilder { .try_into()?, )) .await?; - let proposed_header = target_light_block.into_header(&trusted_light_block); - - let _update_msg = MsgUpdateClient { + let update_msg = MsgUpdateClient { client_id: channel.client_id, client_message: Some(proposed_header.into()), signer: self.signer_address.clone(), }; - todo!() + let all_msgs = std::iter::once(Any::from_msg(&update_msg)) + .chain(timeout_msgs.into_iter().map(|m| Any::from_msg(&m))) + .chain(recv_msgs.into_iter().map(|m| Any::from_msg(&m))) + .chain(ack_msgs.into_iter().map(|m| Any::from_msg(&m))) + .collect::, _>>()?; + + let tx_body = TxBody { + messages: all_msgs, + ..Default::default() + }; + Ok(tx_body.encode_to_vec()) } } diff --git a/packages/relayer-lib/src/tx_builder/eth_eureka.rs b/packages/relayer-lib/src/tx_builder/eth_eureka.rs index b131df83..079080b4 100644 --- a/packages/relayer-lib/src/tx_builder/eth_eureka.rs +++ b/packages/relayer-lib/src/tx_builder/eth_eureka.rs @@ -3,45 +3,29 @@ use std::{env, str::FromStr}; -use alloy::{ - primitives::Address, - providers::Provider, - sol_types::{SolCall, SolValue}, - transports::Transport, -}; +use alloy::{primitives::Address, providers::Provider, sol_types::SolCall, transports::Transport}; use anyhow::Result; -use futures::future; use ibc_core_host_types::identifiers::ChainId; use ibc_eureka_solidity_types::{ ics02::client::clientInstance, ics26::{ - router::{ - ackPacketCall, multicallCall, recvPacketCall, routerCalls, routerInstance, - timeoutPacketCall, - }, + router::{multicallCall, routerCalls, routerInstance}, IICS02ClientMsgs::Height, - IICS26RouterMsgs::{MsgAckPacket, MsgRecvPacket, MsgTimeoutPacket}, - }, - sp1_ics07::{ - sp1_ics07_tendermint, - IICS07TendermintMsgs::ClientState, - IMembershipMsgs::{MembershipProof, SP1MembershipAndUpdateClientProof}, - ISP1Msgs::SP1Proof, }, + sp1_ics07::{sp1_ics07_tendermint, IICS07TendermintMsgs::ClientState}, }; // Re-export the `SupportedProofType` enum. pub use sp1_ics07_tendermint_prover::prover::SupportedProofType; -use sp1_ics07_tendermint_prover::{ - programs::UpdateClientAndMembershipProgram, prover::SP1ICS07TendermintProver, -}; -use sp1_ics07_tendermint_utils::{light_block::LightBlockExt, rpc::TendermintRpcExt}; -use sp1_sdk::HashableKey; +use sp1_ics07_tendermint_utils::rpc::TendermintRpcExt; use tendermint_rpc::HttpClient; use crate::{ chain::{CosmosSdk, EthEureka}, events::EurekaEvent, + utils::eth_eureka::{ + inject_sp1_proof, src_events_to_recv_and_ack_msgs, target_events_to_timeout_msgs, + }, }; use super::r#trait::TxBuilderService; @@ -108,7 +92,6 @@ where T: Transport + Clone, P: Provider + Clone, { - #[allow(clippy::too_many_lines)] #[tracing::instrument(skip_all)] async fn relay_events( &self, @@ -129,125 +112,32 @@ where revisionHeight: revision_height, }; - let filter_channel = target_channel_id.clone(); - let timeout_msgs = dest_events.into_iter().filter_map(|e| match e { - EurekaEvent::SendPacket(se) => { - if now >= se.packet.timeoutTimestamp && se.packet.sourceChannel == filter_channel { - Some(routerCalls::timeoutPacket(timeoutPacketCall { - msg_: MsgTimeoutPacket { - packet: se.packet, - proofHeight: latest_height.clone(), - proofTimeout: b"".into(), - }, - })) - } else { - None - } - } - _ => None, - }); - - // TODO: We might wanna filter out send packets that have been actually received + let timeout_msgs = + target_events_to_timeout_msgs(dest_events, &target_channel_id, &latest_height, now); - let recv_and_ack_msgs = src_events.into_iter().filter_map(|e| match e { - EurekaEvent::SendPacket(se) => { - if se.packet.timeoutTimestamp > now && se.packet.destChannel == filter_channel { - Some(routerCalls::recvPacket(recvPacketCall { - msg_: MsgRecvPacket { - packet: se.packet, - proofHeight: latest_height.clone(), - proofCommitment: b"".into(), - }, - })) - } else { - None - } - } - EurekaEvent::WriteAcknowledgement(we) => { - if we.packet.sourceChannel == filter_channel { - Some(routerCalls::ackPacket(ackPacketCall { - msg_: MsgAckPacket { - packet: we.packet, - acknowledgement: we.acknowledgements[0].clone(), // TODO: handle multiple acks - proofHeight: latest_height.clone(), - proofAcked: b"".into(), - }, - })) - } else { - None - } - } - _ => None, - }); + let recv_and_ack_msgs = + src_events_to_recv_and_ack_msgs(src_events, &target_channel_id, &latest_height, now); - let mut all_msgs = timeout_msgs.chain(recv_and_ack_msgs).collect::>(); + let mut all_msgs = timeout_msgs + .into_iter() + .chain(recv_and_ack_msgs.into_iter()) + .collect::>(); + if all_msgs.is_empty() { + anyhow::bail!("No messages to relay to Ethereum"); + } tracing::debug!("Messages to be relayed to Ethereum: {:?}", all_msgs); - // TODO: Filter already submitted packets - - let ibc_paths = all_msgs - .iter() - .map(|msg| match msg { - routerCalls::timeoutPacket(call) => call.msg_.packet.receipt_commitment_path(), - routerCalls::recvPacket(call) => call.msg_.packet.commitment_path(), - routerCalls::ackPacket(call) => call.msg_.packet.ack_commitment_path(), - _ => unreachable!(), - }) - .map(|path| vec![b"ibc".into(), path]); - - let kv_proofs: Vec<(Vec>, Vec, _)> = - future::try_join_all(ibc_paths.into_iter().map(|path| async { - let (value, proof) = self.tm_client.prove_path(&path, revision_height).await?; - anyhow::Ok((path, value, proof)) - })) - .await?; - let client_state = self.client_state(target_channel_id).await?; - let trusted_light_block = self - .tm_client - .get_light_block(Some(client_state.latestHeight.revisionHeight)) - .await?; - - // Get the proposed header from the target light block. - let proposed_header = latest_light_block.into_header(&trusted_light_block); - - let uc_and_mem_prover = SP1ICS07TendermintProver::::new( - client_state - .zkAlgorithm - .try_into() - .map_err(|e: String| anyhow::anyhow!(e))?, - ); - let uc_and_mem_proof = uc_and_mem_prover.generate_proof( - &client_state, - &trusted_light_block.to_consensus_state().into(), - &proposed_header, + inject_sp1_proof( + &mut all_msgs, + &self.tm_client, + latest_light_block, + client_state, now, - kv_proofs, - ); - - let sp1_proof = MembershipProof::from(SP1MembershipAndUpdateClientProof { - sp1Proof: SP1Proof::new( - &uc_and_mem_prover.vkey.bytes32(), - uc_and_mem_proof.bytes(), - uc_and_mem_proof.public_values.to_vec(), - ), - }); - - // inject proof - match all_msgs.first_mut() { - Some(routerCalls::timeoutPacket(ref mut call)) => { - *call.msg_.proofTimeout = sp1_proof.abi_encode().into(); - } - Some(routerCalls::recvPacket(ref mut call)) => { - *call.msg_.proofCommitment = sp1_proof.abi_encode().into(); - } - Some(routerCalls::ackPacket(ref mut call)) => { - *call.msg_.proofAcked = sp1_proof.abi_encode().into(); - } - _ => unreachable!(), - } + ) + .await?; let calls = all_msgs.into_iter().map(|msg| match msg { routerCalls::timeoutPacket(call) => call.abi_encode(), diff --git a/packages/relayer-lib/src/utils/cosmos.rs b/packages/relayer-lib/src/utils/cosmos.rs new file mode 100644 index 000000000..1f26c96c --- /dev/null +++ b/packages/relayer-lib/src/utils/cosmos.rs @@ -0,0 +1,221 @@ +//! Relayer utilities for `CosmosSDK` chains. + +use anyhow::Result; +use futures::future; +use ibc_eureka_solidity_types::ics26::router::{SendPacket, WriteAcknowledgement}; +use ibc_proto_eureka::{ + ibc::core::{ + channel::v2::{Acknowledgement, MsgAcknowledgement, MsgRecvPacket, MsgTimeout}, + client::v1::Height, + }, + Protobuf, +}; +use sp1_ics07_tendermint_utils::rpc::TendermintRpcExt; +use tendermint_rpc::HttpClient; + +use crate::events::EurekaEvent; + +/// Converts a list of [`EurekaEvent`]s to a list of [`MsgTimeout`]s. +/// # Errors +/// Returns an error if proof cannot be generated, or membership value is empty for a packet. +pub async fn target_events_to_timeout_msgs( + target_events: Vec, + source_tm_client: &HttpClient, + target_channel_id: &str, + revision_number: u64, + target_height: u32, + signer_address: &str, +) -> Result> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + + future::try_join_all( + target_events + .into_iter() + .filter(|e| match e { + EurekaEvent::SendPacket(se) => { + now >= se.packet.timeoutTimestamp + && se.packet.sourceChannel == target_channel_id + } + _ => false, + }) + .map(|e| async { + match e { + EurekaEvent::SendPacket(se) => { + send_event_to_timout_packet( + se, + source_tm_client, + revision_number, + target_height, + signer_address.to_string(), + ) + .await + } + _ => unreachable!(), + } + }), + ) + .await +} + +/// Converts a list of [`EurekaEvent`]s to a list of [`MsgRecvPacket`]s and +/// [`MsgAcknowledgement`]s. +/// # Errors +/// Returns an error if proof cannot be generated, or membership value is empty for a packet. +pub async fn src_events_to_recv_and_ack_msgs( + src_events: Vec, + source_tm_client: &HttpClient, + target_channel_id: &str, + revision_number: u64, + target_height: u32, + signer_address: &str, +) -> Result<(Vec, Vec)> { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + + let (src_send_events, src_ack_events): (Vec<_>, Vec<_>) = src_events + .into_iter() + .filter(|e| match e { + EurekaEvent::SendPacket(se) => { + se.packet.timeoutTimestamp > now && se.packet.destChannel == target_channel_id + } + EurekaEvent::WriteAcknowledgement(we) => we.packet.sourceChannel == target_channel_id, + EurekaEvent::RecvPacket(_) => false, + }) + .partition(|e| match e { + EurekaEvent::SendPacket(_) => true, + EurekaEvent::WriteAcknowledgement(_) => false, + EurekaEvent::RecvPacket(_) => unreachable!(), + }); + + let recv_msgs = future::try_join_all(src_send_events.into_iter().map(|e| async { + match e { + EurekaEvent::SendPacket(se) => { + send_event_to_recv_packet( + se, + source_tm_client, + revision_number, + target_height, + signer_address.to_string(), + ) + .await + } + _ => unreachable!(), + } + })) + .await?; + + let ack_msgs = future::try_join_all(src_ack_events.into_iter().map(|e| async { + match e { + EurekaEvent::WriteAcknowledgement(we) => { + write_ack_event_to_ack_packet( + we, + source_tm_client, + revision_number, + target_height, + signer_address.to_string(), + ) + .await + } + _ => unreachable!(), + } + })) + .await?; + + Ok((recv_msgs, ack_msgs)) +} + +/// Converts a [`SendPacket`] event to a [`MsgRecvPacket`]. +/// This function doesn't check whether the packet is already received or timed out. +/// # Errors +/// Returns an error if proof cannot be generated, or membership value is empty. +async fn send_event_to_recv_packet( + se: SendPacket, + source_tm_client: &HttpClient, + revision_number: u64, + target_height: u32, + signer_address: String, +) -> Result { + let ibc_path = se.packet.commitment_path(); + let (value, proof) = source_tm_client + .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) + .await?; + + if value.is_empty() { + anyhow::bail!("Membership value is empty") + } + Ok(MsgRecvPacket { + packet: Some(se.packet.into()), + proof_height: Some(Height { + revision_number, + revision_height: target_height.into(), + }), + proof_commitment: proof.encode_vec(), + signer: signer_address, + }) +} + +/// Converts a [`SendPacket`] event to a [`MsgTimeout`]. +/// This function doesn't check whether the packet is already received or timed out. +/// # Errors +/// Returns an error if proof cannot be generated, or non-membership value is not empty. +async fn send_event_to_timout_packet( + se: SendPacket, + source_tm_client: &HttpClient, + revision_number: u64, + target_height: u32, + signer_address: String, +) -> Result { + let ibc_path = se.packet.receipt_commitment_path(); + let (value, proof) = source_tm_client + .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) + .await?; + + if !value.is_empty() { + anyhow::bail!("Non-membership value is not empty") + } + Ok(MsgTimeout { + packet: Some(se.packet.into()), + proof_height: Some(Height { + revision_number, + revision_height: target_height.into(), + }), + proof_unreceived: proof.encode_vec(), + signer: signer_address, + }) +} + +/// Converts a [`WriteAcknowledgement`] event to a [`MsgAcknowledgement`]. +/// This function doesn't check whether the packet is already acknowledged. +/// # Errors +/// Returns an error if proof cannot be generated, or membership value is empty. +async fn write_ack_event_to_ack_packet( + we: WriteAcknowledgement, + source_tm_client: &HttpClient, + revision_number: u64, + target_height: u32, + signer_address: String, +) -> Result { + let ibc_path = we.packet.ack_commitment_path(); + let (value, proof) = source_tm_client + .prove_path(&[b"ibc".to_vec(), ibc_path], target_height) + .await?; + + if value.is_empty() { + anyhow::bail!("Membership value is empty") + } + Ok(MsgAcknowledgement { + packet: Some(we.packet.into()), + acknowledgement: Some(Acknowledgement { + app_acknowledgements: we.acknowledgements.into_iter().map(Into::into).collect(), + }), + proof_height: Some(Height { + revision_number, + revision_height: target_height.into(), + }), + proof_acked: proof.encode_vec(), + signer: signer_address, + }) +} diff --git a/packages/relayer-lib/src/utils/eth_eureka.rs b/packages/relayer-lib/src/utils/eth_eureka.rs new file mode 100644 index 000000000..d3e2ab0d --- /dev/null +++ b/packages/relayer-lib/src/utils/eth_eureka.rs @@ -0,0 +1,177 @@ +//! Relayer utilities for `solidity-ibc-eureka` chains. + +use alloy::{primitives::Bytes, sol_types::SolValue}; +use anyhow::Result; +use futures::future; +use ibc_eureka_solidity_types::{ + ics26::{ + router::{ackPacketCall, recvPacketCall, routerCalls}, + IICS02ClientMsgs::Height, + IICS26RouterMsgs::{MsgAckPacket, MsgRecvPacket, MsgTimeoutPacket}, + }, + sp1_ics07::{ + IICS07TendermintMsgs::ClientState, + IMembershipMsgs::{MembershipProof, SP1MembershipAndUpdateClientProof}, + ISP1Msgs::SP1Proof, + }, +}; +use sp1_ics07_tendermint_prover::{ + programs::UpdateClientAndMembershipProgram, prover::SP1ICS07TendermintProver, +}; +use sp1_ics07_tendermint_utils::{light_block::LightBlockExt, rpc::TendermintRpcExt}; +use sp1_sdk::HashableKey; +use tendermint_light_client_verifier::types::LightBlock; +use tendermint_rpc::HttpClient; + +use crate::events::EurekaEvent; + +/// Converts a list of [`EurekaEvent`]s to a list of [`routerCalls::timeoutPacket`]s with empty +/// proofs. +pub fn target_events_to_timeout_msgs( + target_events: Vec, + target_channel_id: &str, + target_height: &Height, + now: u64, +) -> Vec { + target_events + .into_iter() + .filter_map(|e| match e { + EurekaEvent::SendPacket(se) => { + if now >= se.packet.timeoutTimestamp && se.packet.sourceChannel == target_channel_id + { + Some(routerCalls::timeoutPacket( + ibc_eureka_solidity_types::ics26::router::timeoutPacketCall { + msg_: MsgTimeoutPacket { + packet: se.packet, + proofHeight: target_height.clone(), + proofTimeout: Bytes::default(), + }, + }, + )) + } else { + None + } + } + _ => None, + }) + .collect() +} + +/// Converts a list of [`EurekaEvent`]s to a list of [`routerCalls::recvPacket`]s and +/// [`routerCalls::ackPacket`]s with empty proofs. +pub fn src_events_to_recv_and_ack_msgs( + src_events: Vec, + target_channel_id: &str, + target_height: &Height, + now: u64, +) -> Vec { + src_events + .into_iter() + .filter_map(|e| match e { + EurekaEvent::SendPacket(se) => { + if se.packet.timeoutTimestamp > now && se.packet.destChannel == target_channel_id { + Some(routerCalls::recvPacket(recvPacketCall { + msg_: MsgRecvPacket { + packet: se.packet, + proofHeight: target_height.clone(), + proofCommitment: Bytes::default(), + }, + })) + } else { + None + } + } + EurekaEvent::WriteAcknowledgement(we) => { + if we.packet.sourceChannel == target_channel_id { + Some(routerCalls::ackPacket(ackPacketCall { + msg_: MsgAckPacket { + packet: we.packet, + acknowledgement: we.acknowledgements[0].clone(), // TODO: handle multiple acks + proofHeight: target_height.clone(), + proofAcked: Bytes::default(), + }, + })) + } else { + None + } + } + EurekaEvent::RecvPacket(_) => None, + }) + .collect() +} + +/// Generates and injects an SP1 proof into the first message in `msgs`. +/// # Errors +/// Returns an error if the sp1 proof cannot be generated. +pub async fn inject_sp1_proof( + msgs: &mut [routerCalls], + tm_client: &HttpClient, + target_light_block: LightBlock, + client_state: ClientState, + now: u64, +) -> Result<()> { + let target_height = u32::try_from(target_light_block.height().value())?; + + let ibc_paths = msgs + .iter() + .map(|msg| match msg { + routerCalls::timeoutPacket(call) => call.msg_.packet.receipt_commitment_path(), + routerCalls::recvPacket(call) => call.msg_.packet.commitment_path(), + routerCalls::ackPacket(call) => call.msg_.packet.ack_commitment_path(), + _ => unreachable!(), + }) + .map(|path| vec![b"ibc".into(), path]); + + let kv_proofs: Vec<(Vec>, Vec, _)> = + future::try_join_all(ibc_paths.into_iter().map(|path| async { + let (value, proof) = tm_client.prove_path(&path, target_height).await?; + anyhow::Ok((path, value, proof)) + })) + .await?; + + let trusted_light_block = tm_client + .get_light_block(Some(client_state.latestHeight.revisionHeight)) + .await?; + + // Get the proposed header from the target light block. + let proposed_header = target_light_block.into_header(&trusted_light_block); + + let uc_and_mem_prover = SP1ICS07TendermintProver::::new( + client_state + .zkAlgorithm + .try_into() + .map_err(|e: String| anyhow::anyhow!(e))?, + ); + + let uc_and_mem_proof = uc_and_mem_prover.generate_proof( + &client_state, + &trusted_light_block.to_consensus_state().into(), + &proposed_header, + now, + kv_proofs, + ); + + let sp1_proof = MembershipProof::from(SP1MembershipAndUpdateClientProof { + sp1Proof: SP1Proof::new( + &uc_and_mem_prover.vkey.bytes32(), + uc_and_mem_proof.bytes(), + uc_and_mem_proof.public_values.to_vec(), + ), + }); + + // inject proof + match msgs.first_mut() { + Some(routerCalls::timeoutPacket(ref mut call)) => { + *call.msg_.proofTimeout = sp1_proof.abi_encode().into(); + } + Some(routerCalls::recvPacket(ref mut call)) => { + *call.msg_.proofCommitment = sp1_proof.abi_encode().into(); + } + Some(routerCalls::ackPacket(ref mut call)) => { + *call.msg_.proofAcked = sp1_proof.abi_encode().into(); + } + _ => unreachable!(), + } + + Ok(()) +} diff --git a/packages/relayer-lib/src/utils/mod.rs b/packages/relayer-lib/src/utils/mod.rs new file mode 100644 index 000000000..61ccfca5 --- /dev/null +++ b/packages/relayer-lib/src/utils/mod.rs @@ -0,0 +1,4 @@ +//! This module contains the utilities for relayer implementations. + +pub mod cosmos; +pub mod eth_eureka;