diff --git a/Cargo.lock b/Cargo.lock index fe5fcba34..8685bbd9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4018,6 +4018,7 @@ dependencies = [ "ibc-eureka-solidity-types 0.1.0", "ibc-eureka-utils 0.1.0", "ibc-proto 0.51.1 (git+https://github.com/srdtrk/ibc-proto-rs?rev=7074fb20c3a654e945e5ca46627c5a971d785e04)", + "ics23", "prost", "serde", "serde_json", @@ -4027,6 +4028,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tendermint-rpc", + "tonic 0.13.1", "tracing", ] diff --git a/packages/relayer/lib/Cargo.toml b/packages/relayer/lib/Cargo.toml index 94fda8252..32d8b26c5 100644 --- a/packages/relayer/lib/Cargo.toml +++ b/packages/relayer/lib/Cargo.toml @@ -24,6 +24,7 @@ anyhow = { workspace = true, features = ["std"] } futures = { workspace = true, default-features = true } futures-timer = { workspace = true } tracing = { workspace = true, default-features = true } +tonic = { workspace = true } tendermint = { workspace = true, features = ["std"] } tendermint-rpc = { workspace = true, features = ["http-client"] } @@ -31,6 +32,7 @@ tendermint-light-client-verifier = { workspace = true } ibc-proto-eureka = { workspace = true } ibc-core-commitment-types = { workspace = true } +ics23 = { workspace = true } alloy = { workspace = true, features = ["full", "node-bindings"] } diff --git a/packages/relayer/lib/src/lib.rs b/packages/relayer/lib/src/lib.rs index 01a70c1c0..fec244d6c 100644 --- a/packages/relayer/lib/src/lib.rs +++ b/packages/relayer/lib/src/lib.rs @@ -12,5 +12,7 @@ use ibc_core_commitment_types as _; pub mod chain; pub mod events; pub mod listener; +pub mod service_utils; +pub mod tendermint_client; pub mod tx_builder; pub mod utils; diff --git a/packages/relayer/lib/src/service_utils.rs b/packages/relayer/lib/src/service_utils.rs new file mode 100644 index 000000000..e2996236f --- /dev/null +++ b/packages/relayer/lib/src/service_utils.rs @@ -0,0 +1,46 @@ +//! Common service patterns and utilities for relayer modules +//! This module provides shared functionality for `RelayerService` implementations + +use tendermint::Hash; +use tonic::Status; + +/// Convert `anyhow::Error` to `tonic::Status` +#[inline] +#[must_use] +pub fn to_tonic_status(err: anyhow::Error) -> Status { + Status::from_error(err.into()) +} + +/// Parse Cosmos transaction hashes from request +/// +/// # Errors +/// +/// Returns a `Status` error if any transaction ID cannot be parsed as a valid hash +#[inline] +#[allow(clippy::result_large_err)] +pub fn parse_cosmos_tx_hashes(tx_ids: Vec>) -> Result, Status> { + tx_ids + .into_iter() + .map(Hash::try_from) + .collect::, _>>() + .map_err(|e| Status::from_error(e.into())) +} + +/// Parse Ethereum transaction hashes from request +/// +/// # Errors +/// +/// Returns a `Status` error if any transaction ID is not exactly 32 bytes +#[inline] +#[allow(clippy::result_large_err)] +pub fn parse_eth_tx_hashes(tx_ids: Vec>) -> Result, Status> { + tx_ids + .into_iter() + .map(|tx_id| { + tx_id + .try_into() + .map_err(|tx| format!("invalid tx hash: {tx:?}")) + }) + .collect::, _>>() + .map_err(|e| Status::from_error(e.into())) +} diff --git a/packages/relayer/lib/src/tendermint_client.rs b/packages/relayer/lib/src/tendermint_client.rs new file mode 100644 index 000000000..1842955f5 --- /dev/null +++ b/packages/relayer/lib/src/tendermint_client.rs @@ -0,0 +1,59 @@ +//! Utilities for Tendermint light client configuration +//! This module provides common functionality for creating and configuring Tendermint client states + +use ibc_proto_eureka::{ + google::protobuf::Duration, + ibc::{ + core::client::v1::Height, + lightclients::tendermint::v1::{ClientState, Fraction}, + }, +}; + +/// Default trust level for Tendermint light clients (1/3) +#[must_use] +pub const fn default_trust_level() -> Fraction { + Fraction { + numerator: 1, + denominator: 3, + } +} + +/// Default max clock drift for Tendermint light clients (15 seconds) +#[must_use] +pub const fn default_max_clock_drift() -> Duration { + Duration { + seconds: 15, + nanos: 0, + } +} + +/// Build a Tendermint client state with common defaults +/// +/// # Arguments +/// * `chain_id` - The chain ID +/// * `height` - The latest height +/// * `trusting_period` - The trusting period +/// * `unbonding_period` - The unbonding period +/// * `proof_specs` - The proof specifications for ICS23 +/// +/// Returns a `ClientState` with default trust level, max clock drift, and the provided proof specs +#[must_use] +pub fn build_tendermint_client_state( + chain_id: String, + height: Height, + trusting_period: Duration, + unbonding_period: Duration, + proof_specs: Vec, +) -> ClientState { + ClientState { + chain_id, + trust_level: Some(default_trust_level()), + trusting_period: Some(trusting_period), + unbonding_period: Some(unbonding_period), + max_clock_drift: Some(default_max_clock_drift()), + latest_height: Some(height), + proof_specs, + upgrade_path: vec!["upgrade".to_string(), "upgradedIBCState".to_string()], + ..Default::default() + } +} diff --git a/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs b/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs index 022506a3c..e34f56b3f 100644 --- a/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs @@ -8,16 +8,19 @@ unused_crate_dependencies )] +use ics23 as _; +use tendermint as _; + pub mod tx_builder; use std::collections::HashMap; use ibc_eureka_relayer_lib::{ listener::{cosmos_sdk, ChainListenerService}, + service_utils::{parse_cosmos_tx_hashes, to_tonic_status}, tx_builder::TxBuilderService, }; use ibc_eureka_utils::rpc::TendermintRpcExt; -use tendermint::Hash; use tendermint_rpc::HttpClient; use tonic::{Request, Response}; @@ -85,7 +88,7 @@ impl RelayerService for CosmosToCosmosRelayerModuleService { .target_listener .chain_id() .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + .map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: String::new(), }), @@ -94,7 +97,7 @@ impl RelayerService for CosmosToCosmosRelayerModuleService { .src_listener .chain_id() .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + .map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: String::new(), }), @@ -112,19 +115,9 @@ impl RelayerService for CosmosToCosmosRelayerModuleService { let inner_req = request.into_inner(); tracing::info!("Got {} source tx IDs", inner_req.source_tx_ids.len()); tracing::info!("Got {} timeout tx IDs", inner_req.timeout_tx_ids.len()); - let src_txs = inner_req - .source_tx_ids - .into_iter() - .map(Hash::try_from) - .collect::, _>>() - .map_err(|e| tonic::Status::from_error(e.into()))?; + let src_txs = parse_cosmos_tx_hashes(inner_req.source_tx_ids)?; - let target_txs = inner_req - .timeout_tx_ids - .into_iter() - .map(Hash::try_from) - .collect::, _>>() - .map_err(|e| tonic::Status::from_error(e.into()))?; + let target_txs = parse_cosmos_tx_hashes(inner_req.timeout_tx_ids)?; let src_events = self .src_listener diff --git a/packages/relayer/modules/cosmos-to-cosmos/src/tx_builder.rs b/packages/relayer/modules/cosmos-to-cosmos/src/tx_builder.rs index a1fd65fca..6002c8b43 100644 --- a/packages/relayer/modules/cosmos-to-cosmos/src/tx_builder.rs +++ b/packages/relayer/modules/cosmos-to-cosmos/src/tx_builder.rs @@ -11,14 +11,15 @@ use ibc_proto_eureka::{ google::protobuf::{Any, Duration}, ibc::{ core::client::v1::{Height, MsgCreateClient, MsgUpdateClient}, - lightclients::tendermint::v1::{ClientState, Fraction}, + lightclients::tendermint::v1::ClientState, }, }; use prost::Message; use tendermint_rpc::HttpClient; use ibc_eureka_relayer_lib::{ - chain::CosmosSdk, events::EurekaEventWithHeight, tx_builder::TxBuilderService, utils::cosmos, + chain::CosmosSdk, events::EurekaEventWithHeight, + tendermint_client::build_tendermint_client_state, tx_builder::TxBuilderService, utils::cosmos, }; /// The `TxBuilder` produces txs to [`CosmosSdk`] based on events from [`CosmosSdk`]. @@ -167,37 +168,28 @@ impl TxBuilderService for TxBuilder { revision_number: chain_id.revision_number(), revision_height: latest_light_block.height().value(), }; - let default_trust_level = Fraction { - numerator: 1, - denominator: 3, - }; - let default_max_clock_drift = Duration { - seconds: 15, - nanos: 0, - }; let unbonding_period = self .source_tm_client .sdk_staking_params() .await? .unbonding_time .ok_or_else(|| anyhow::anyhow!("No unbonding time found"))?; + // Defaults to the recommended 2/3 of the UnbondingPeriod let trusting_period = Duration { seconds: 2 * (unbonding_period.seconds / 3), nanos: 0, }; - let client_state = ClientState { - chain_id: chain_id.to_string(), - trust_level: Some(default_trust_level), - trusting_period: Some(trusting_period), - unbonding_period: Some(unbonding_period), - max_clock_drift: Some(default_max_clock_drift), - latest_height: Some(height), - proof_specs: vec![ics23::iavl_spec(), ics23::tendermint_spec()], - upgrade_path: vec!["upgrade".to_string(), "upgradedIBCState".to_string()], - ..Default::default() - }; + let proof_specs = vec![ics23::iavl_spec(), ics23::tendermint_spec()]; + + let client_state = build_tendermint_client_state( + chain_id.to_string(), + height, + trusting_period, + unbonding_period, + proof_specs, + ); let consensus_state = latest_light_block.to_consensus_state(); diff --git a/packages/relayer/modules/cosmos-to-eth/src/lib.rs b/packages/relayer/modules/cosmos-to-eth/src/lib.rs index f2e44982e..9ff6dc2af 100644 --- a/packages/relayer/modules/cosmos-to-eth/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-eth/src/lib.rs @@ -8,6 +8,8 @@ unused_crate_dependencies )] +use tendermint as _; + pub mod tx_builder; use alloy::{ @@ -16,6 +18,7 @@ use alloy::{ }; use ibc_eureka_relayer_lib::{ listener::{cosmos_sdk, eth_eureka, ChainListenerService}, + service_utils::{parse_cosmos_tx_hashes, to_tonic_status}, tx_builder::TxBuilderService, }; use ibc_eureka_utils::rpc::TendermintRpcExt; @@ -25,7 +28,6 @@ use sp1_ics07_tendermint_prover::programs::{ }; use sp1_prover::components::CpuProverComponents; use sp1_sdk::{Prover, ProverClient}; -use tendermint::Hash; use tendermint_rpc::HttpClient; use tonic::{Request, Response}; use tx_builder::TxBuilder; @@ -249,16 +251,12 @@ impl RelayerService for CosmosToEthRelayerModuleService { .eth_listener .chain_id() .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + .map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: self.tx_builder.ics26_router.address().to_string(), }), source_chain: Some(api::Chain { - chain_id: self - .tm_listener - .chain_id() - .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + chain_id: self.tm_listener.chain_id().await.map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: String::new(), }), @@ -276,12 +274,7 @@ impl RelayerService for CosmosToEthRelayerModuleService { let inner_req = request.into_inner(); tracing::info!("Got {} source tx IDs", inner_req.source_tx_ids.len()); tracing::info!("Got {} timeout tx IDs", inner_req.timeout_tx_ids.len()); - let cosmos_txs = inner_req - .source_tx_ids - .into_iter() - .map(Hash::try_from) - .collect::, _>>() - .map_err(|e| tonic::Status::from_error(e.into()))?; + let cosmos_txs = parse_cosmos_tx_hashes(inner_req.source_tx_ids)?; let eth_txs = inner_req .timeout_tx_ids diff --git a/packages/relayer/modules/eth-to-cosmos/src/lib.rs b/packages/relayer/modules/eth-to-cosmos/src/lib.rs index 3279d3986..cf5f9b294 100644 --- a/packages/relayer/modules/eth-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/eth-to-cosmos/src/lib.rs @@ -8,6 +8,8 @@ unused_crate_dependencies )] +use tendermint as _; + pub mod tx_builder; use std::collections::HashMap; @@ -19,10 +21,10 @@ use alloy::{ use ibc_eureka_relayer_lib::{ events::EurekaEventWithHeight, listener::{cosmos_sdk, eth_eureka, ChainListenerService}, + service_utils::{parse_cosmos_tx_hashes, parse_eth_tx_hashes, to_tonic_status}, tx_builder::TxBuilderService, }; use ibc_eureka_utils::rpc::TendermintRpcExt; -use tendermint::Hash; use tendermint_rpc::HttpClient; use tonic::{Request, Response}; @@ -114,11 +116,7 @@ impl RelayerService for EthToCosmosRelayerModuleService { tracing::info!("Handling info request for Eth to Cosmos..."); Ok(Response::new(api::InfoResponse { target_chain: Some(api::Chain { - chain_id: self - .tm_listener - .chain_id() - .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + chain_id: self.tm_listener.chain_id().await.map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: String::new(), }), @@ -127,7 +125,7 @@ impl RelayerService for EthToCosmosRelayerModuleService { .eth_listener .chain_id() .await - .map_err(|e| tonic::Status::from_error(e.into()))?, + .map_err(to_tonic_status)?, ibc_version: "2".to_string(), ibc_contract: self.tx_builder.ics26_router_address().to_string(), }), @@ -145,20 +143,10 @@ impl RelayerService for EthToCosmosRelayerModuleService { let inner_req = request.into_inner(); tracing::info!("Got {} source tx IDs", inner_req.source_tx_ids.len()); tracing::info!("Got {} timeout tx IDs", inner_req.timeout_tx_ids.len()); - let eth_txs = inner_req - .source_tx_ids - .into_iter() - .map(TryInto::<[u8; 32]>::try_into) - .map(|tx_hash| tx_hash.map(TxHash::from)) - .collect::, _>>() - .map_err(|tx| tonic::Status::from_error(format!("invalid tx hash: {tx:?}").into()))?; - - let cosmos_txs = inner_req - .timeout_tx_ids - .into_iter() - .map(Hash::try_from) - .collect::, _>>() - .map_err(|e| tonic::Status::from_error(e.into()))?; + let eth_tx_hashes = parse_eth_tx_hashes(inner_req.source_tx_ids)?; + let eth_txs = eth_tx_hashes.into_iter().map(TxHash::from).collect(); + + let cosmos_txs = parse_cosmos_tx_hashes(inner_req.timeout_tx_ids)?; let eth_events = self .eth_listener diff --git a/packages/relayer/modules/eth-to-cosmos/src/tx_builder.rs b/packages/relayer/modules/eth-to-cosmos/src/tx_builder.rs index 5b94f8f0a..346569d2f 100644 --- a/packages/relayer/modules/eth-to-cosmos/src/tx_builder.rs +++ b/packages/relayer/modules/eth-to-cosmos/src/tx_builder.rs @@ -453,15 +453,15 @@ where .compute_sync_committee_period_at_slot(ethereum_client_state.latest_slot); let latest_period = ethereum_client_state.compute_sync_committee_period_at_slot(proof_slot); tracing::info!( - "Relay events summary: + "Relay events summary: client id: {}, - recv events processed: #{}, - ack events processed: #{}, - timeout events processed: #{}, - initial slot: {}, - latest trusted slot (after updates): {}, - initial period: {}, - latest period: {}, + recv events processed: #{}, + ack events processed: #{}, + timeout events processed: #{}, + initial slot: {}, + latest trusted slot (after updates): {}, + initial period: {}, + latest period: {}, number of headers: #{}", dst_client_id, recv_msgs.len(), @@ -649,12 +649,12 @@ where .compute_sync_committee_period_at_slot(ethereum_client_state.latest_slot); let latest_period = ethereum_client_state.compute_sync_committee_period_at_slot(proof_slot); tracing::info!( - "Update client summary: + "Update client summary: client id: {}, - initial slot: {}, - latest trusted slot (after updates): {}, - initial period: {}, - latest period: {}, + initial slot: {}, + latest trusted slot (after updates): {}, + initial period: {}, + latest period: {}, number of headers: #{}", dst_client_id, ethereum_client_state.latest_slot,