diff --git a/CHANGELOG.md b/CHANGELOG.md index 72cfb2e1ed..f4611fa421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,13 @@ Special thanks to external contributors for this release: @CharlyCst ([#347]). - [relayer] Integrate relayer spike into relayer crate ([#335]) - [modules] Implement flexible connection id selection ([#332]) - [relayer] Implement `query_header_at_height` via plain RPC queries (no light client verification) ([#336]) +- [relayer-cli] Merge light clients config in relayer config and add commands to add/remove light clients ([#348]) [#274]: https://github.com/informalsystems/ibc-rs/issues/274 [#332]: https://github.com/informalsystems/ibc-rs/issues/332 [#335]: https://github.com/informalsystems/ibc-rs/pulls/335 [#336]: https://github.com/informalsystems/ibc-rs/issues/336 +[#348]: https://github.com/informalsystems/ibc-rs/pulls/348 ### IMPROVEMENTS diff --git a/relayer-cli/src/commands/light.rs b/relayer-cli/src/commands/light.rs index 1c2cfbb846..d37c2b6a89 100644 --- a/relayer-cli/src/commands/light.rs +++ b/relayer-cli/src/commands/light.rs @@ -2,12 +2,17 @@ use abscissa_core::{Command, Options, Runnable}; -mod init; +mod add; +mod rm; /// `light` subcommand #[derive(Command, Debug, Options, Runnable)] pub enum LightCmd { - /// The `light init` subcommand - #[options(help = "initiate a light client for a given chain")] - Init(init::InitCmd), + /// The `light add` subcommand + #[options(help = "add a light client peer for a given chain")] + Add(add::AddCmd), + + /// The `light rm` subcommand + #[options(help = "remove a light client peer for a given chain")] + Rm(rm::RmCmd), } diff --git a/relayer-cli/src/commands/light/add.rs b/relayer-cli/src/commands/light/add.rs new file mode 100644 index 0000000000..4ef0d3a3f7 --- /dev/null +++ b/relayer-cli/src/commands/light/add.rs @@ -0,0 +1,220 @@ +use std::{fmt, io, io::Write, ops::Deref}; + +use abscissa_core::{application::fatal_error, error::BoxError, Command, Options, Runnable}; + +use relayer::{ + config::{Config, LightClientConfig, PeersConfig}, + util::block_on, +}; +use tendermint::chain::Id as ChainId; +use tendermint::hash::Hash; +use tendermint::{block::Height, net}; +use tendermint_light_client::types::PeerId; +use tendermint_rpc::{Client, HttpClient}; + +use crate::prelude::*; + +#[derive(Command, Debug, Options)] +pub struct AddCmd { + /// RPC network address + #[options(free)] + address: Option, + + /// identifier of the chain + #[options(short = "c")] + chain_id: Option, + + /// whether this is the primary peer + primary: bool, + + /// allow overriding an existing peer + force: bool, + + /// skip confirmation + yes: bool, +} + +#[derive(Clone, Debug)] +struct AddOptions { + /// identifier of the chain + chain_id: ChainId, + + /// RPC network address + address: net::Address, + + /// whether this is the primary peer or not + primary: bool, + + /// allow overriding an existing peer + force: bool, + + /// skip confirmation + yes: bool, +} + +impl AddOptions { + fn from_cmd(cmd: &AddCmd) -> Result { + let chain_id = cmd.chain_id.clone().ok_or("missing chain identifier")?; + let address = cmd.address.clone().ok_or("missing RPC network address")?; + let primary = cmd.primary; + let force = cmd.force; + let yes = cmd.yes; + + Ok(AddOptions { + chain_id, + address, + primary, + force, + yes, + }) + } +} + +#[derive(Debug, Clone)] +pub struct NodeStatus { + chain_id: ChainId, + address: net::Address, + peer_id: PeerId, + latest_hash: Hash, + latest_height: Height, +} + +impl fmt::Display for NodeStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, " chain id: {}", self.chain_id)?; + writeln!(f, " address: {}", self.address)?; + writeln!(f, " peer id: {}", self.peer_id)?; + writeln!(f, " height: {}", self.latest_height)?; + writeln!(f, " hash: {}", self.latest_hash)?; + + Ok(()) + } +} + +fn confirm(status: &NodeStatus, primary: bool) -> Result { + print!("Light client configuration:\n{}", status); + println!(" primary: {}", primary); + + loop { + print!("\n? Do you want to add a new light client with these trust options? (y/n) > "); + io::stdout().flush()?; // need to flush stdout since stdout is often line-buffered + + let mut choice = String::new(); + io::stdin().read_line(&mut choice)?; + + match choice.trim_end() { + "y" | "yes" => return Ok(true), + "n" | "no" => return Ok(false), + _ => continue, + } + } +} + +fn add(mut config: Config, options: AddOptions) -> Result<(), BoxError> { + let status = fetch_status(options.chain_id.clone(), options.address.clone())?; + + if !(options.yes || confirm(&status, options.primary)?) { + return Ok(()); + } + + let new_primary = update_config(options, status.clone(), &mut config)?; + + let config_path = crate::config::config_path()?; + relayer::config::store(&config, config_path)?; + + status_ok!( + "Success", + "Added light client:\n{} primary: {}", + status, + status.peer_id == new_primary, + ); + + Ok(()) +} + +fn fetch_status(chain_id: ChainId, address: net::Address) -> Result { + let rpc_client = HttpClient::new(address.clone())?; + let response = block_on(rpc_client.status())?; + + let peer_id = response.node_info.id; + let latest_height = response.sync_info.latest_block_height; + let latest_hash = response + .sync_info + .latest_block_hash + .ok_or_else(|| "missing latest block hash in RPC status response")?; + + Ok(NodeStatus { + chain_id, + address, + peer_id, + latest_hash, + latest_height, + }) +} + +fn update_config( + options: AddOptions, + status: NodeStatus, + config: &mut Config, +) -> Result { + let chain_config = config + .chains + .iter_mut() + .find(|c| c.id == options.chain_id) + .ok_or_else(|| format!("could not find config for chain: {}", options.chain_id))?; + + let peers_config = chain_config.peers.get_or_insert_with(|| PeersConfig { + primary: status.peer_id, + light_clients: vec![], + }); + + // Check if the given peer exists already, in which case throw an error except if the + // --force flag is set. + let peer_exists = peers_config.light_client(status.peer_id).is_some(); + if peer_exists && !options.force { + return Err(format!( + "a peer with id {} already exists, remove it first \ + or pass the --force flag to override it", + status.peer_id + ) + .into()); + } + + let light_client_config = LightClientConfig { + peer_id: status.peer_id, + address: status.address.clone(), + trusted_header_hash: status.latest_hash, + trusted_height: status.latest_height, + }; + + if peer_exists { + // Filter out the light client config with the specified peer id + peers_config + .light_clients + .retain(|p| p.peer_id != status.peer_id); + } + + peers_config.light_clients.push(light_client_config); + + if options.primary { + peers_config.primary = status.peer_id; + } + + Ok(peers_config.primary) +} + +impl AddCmd { + fn cmd(&self) -> Result<(), BoxError> { + let config = (*app_config()).clone(); + let options = AddOptions::from_cmd(self).map_err(|e| format!("invalid options: {}", e))?; + + add(config, options) + } +} + +impl Runnable for AddCmd { + fn run(&self) { + self.cmd() + .unwrap_or_else(|e| fatal_error(app_reader().deref(), &*e)) + } +} diff --git a/relayer-cli/src/commands/light/init.rs b/relayer-cli/src/commands/light/init.rs deleted file mode 100644 index 6d1c0de15b..0000000000 --- a/relayer-cli/src/commands/light/init.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{future::Future, ops::Deref}; - -use crate::light::config::{LightConfig, LIGHT_CONFIG_PATH}; -use crate::prelude::*; - -use abscissa_core::{application::fatal_error, error::BoxError, Command, Options, Runnable}; - -use tendermint::block::Height; -use tendermint::chain::Id as ChainId; -use tendermint::hash::Hash; -use tendermint_light_client::types::TrustThreshold; - -use relayer::client::trust_options::TrustOptions; - -#[derive(Command, Debug, Options)] -pub struct InitCmd { - #[options(free, help = "identifier of the chain")] - chain_id: Option, - - #[options(help = "trusted header hash", short = "x")] - hash: Option, - - #[options(help = "trusted header height", short = "h")] - height: Option, -} - -#[derive(Clone, Debug)] -struct InitOptions { - /// identifier of the chain - chain_id: ChainId, - - /// trusted header hash - trusted_hash: Hash, - - /// trusted header height - trusted_height: Height, -} - -impl InitOptions { - fn from_init_cmd(cmd: &InitCmd) -> Result { - match (cmd.chain_id.clone(), cmd.hash, cmd.height) { - (Some(chain_id), Some(hash), Some(height)) => Ok(Self { - chain_id, - trusted_hash: hash, - trusted_height: height, - }), - (None, _, _) => Err("missing chain identifier"), - (_, None, _) => Err("missing trusted hash"), - (_, _, None) => Err("missing trusted height"), - } - } -} - -impl InitCmd { - fn cmd(&self) -> Result<(), BoxError> { - let config = app_config(); - - let options = - InitOptions::from_init_cmd(self).map_err(|e| format!("invalid options: {}", e))?; - - let chain_config = config - .chains - .iter() - .find(|c| c.id == options.chain_id) - .ok_or_else(|| format!("could not find config for chain: {}", options.chain_id))?; - - let trust_options = TrustOptions::new( - options.trusted_hash, - options.trusted_height, - chain_config.trusting_period, - TrustThreshold::default(), - )?; - - let mut config = LightConfig::load(LIGHT_CONFIG_PATH)?; - config.chains.insert(chain_config.id.clone(), trust_options); - config.save(LIGHT_CONFIG_PATH)?; - - status_ok!( - chain_config.id.clone(), - "Set trusted options: hash={} height={}", - options.trusted_hash, - options.trusted_height - ); - - Ok(()) - } -} - -impl Runnable for InitCmd { - /// Initialize the light client for the given chain - fn run(&self) { - self.cmd() - .unwrap_or_else(|e| fatal_error(app_reader().deref(), &*e)) - } -} - -fn block_on(future: F) -> F::Output { - tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap() - .block_on(future) -} diff --git a/relayer-cli/src/commands/light/rm.rs b/relayer-cli/src/commands/light/rm.rs new file mode 100644 index 0000000000..0b521b10f5 --- /dev/null +++ b/relayer-cli/src/commands/light/rm.rs @@ -0,0 +1,119 @@ +use std::ops::Deref; + +use crate::prelude::*; + +use abscissa_core::{application::fatal_error, error::BoxError, Command, Options, Runnable}; + +use relayer::config::Config; +use tendermint::chain::Id as ChainId; +use tendermint_light_client::types::PeerId; + +#[derive(Command, Debug, Options)] +pub struct RmCmd { + /// peer id for this client + #[options(free)] + peer_id: Option, + + #[options(short = "c")] + /// identifier of the chain + chain_id: Option, + + /// force removal of primary peer + #[options(short = "f")] + force: bool, +} + +#[derive(Clone, Debug)] +struct RmOptions { + /// identifier of the chain + chain_id: ChainId, + + /// peer id for this client + peer_id: PeerId, + + /// force removal of primary peer + force: bool, +} + +impl RmOptions { + fn from_cmd(cmd: &RmCmd) -> Result { + let chain_id = cmd.chain_id.clone().ok_or("missing chain identifier")?; + let peer_id = cmd.peer_id.ok_or("missing peer identifier")?; + let force = cmd.force; + + Ok(RmOptions { + chain_id, + peer_id, + force, + }) + } +} + +impl RmCmd { + fn update_config(options: RmOptions, config: &mut Config) -> Result { + let chain_config = config + .chains + .iter_mut() + .find(|c| c.id == options.chain_id) + .ok_or_else(|| format!("could not find config for chain: {}", options.chain_id))?; + + let peers_config = chain_config + .peers + .as_mut() + .ok_or_else(|| format!("no peers configured for chain: {}", options.chain_id))?; + + // Check if the given peer actually exists already, if not throw an error. + let peer_exists = peers_config.light_client(options.peer_id).is_some(); + if !peer_exists { + return Err(format!("cannot find peer: {}", options.peer_id).into()); + } + + // Only allow remove the primary peer if the --force option is set + let is_primary = peers_config.primary == options.peer_id; + if is_primary && !options.force { + return Err("cannot remove primary peer, pass --force flag to force removal".into()); + } + + // Filter out the light client config with the specified peer id + peers_config + .light_clients + .retain(|p| p.peer_id != options.peer_id); + + // Disallow removing the last remaining peer + if peers_config.light_clients.is_empty() { + return Err( + "cannot remove last remaining peer, add other peers before removing this one" + .into(), + ); + } + + // If the peer we removed was the primary peer, use the next available peer as the primary + if is_primary { + let new_primary = peers_config.light_clients.first().unwrap(); // SAFETY: safe because of check above + peers_config.primary = new_primary.peer_id; + } + + Ok(options.peer_id) + } + + fn cmd(&self) -> Result<(), BoxError> { + let options = RmOptions::from_cmd(self).map_err(|e| format!("invalid options: {}", e))?; + let mut config = (*app_config()).clone(); + + let removed_peer = Self::update_config(options, &mut config)?; + + let config_path = crate::config::config_path()?; + relayer::config::store(&config, config_path)?; + + status_ok!("Removed", "light client peer '{}'", removed_peer); + + Ok(()) + } +} + +impl Runnable for RmCmd { + fn run(&self) { + self.cmd() + .unwrap_or_else(|e| fatal_error(app_reader().deref(), &*e)) + } +} diff --git a/relayer-cli/src/commands/start.rs b/relayer-cli/src/commands/start.rs index 245663b556..2d65f50bc4 100644 --- a/relayer-cli/src/commands/start.rs +++ b/relayer-cli/src/commands/start.rs @@ -6,12 +6,7 @@ use abscissa_core::{ use relayer::{chain::CosmosSDKChain, config::Config}; -use crate::{ - application::APPLICATION, - light::config::{LightConfig, LIGHT_CONFIG_PATH}, - prelude::*, - tasks, -}; +use crate::{application::APPLICATION, prelude::*, tasks}; #[derive(Command, Debug, Options)] pub struct StartCmd { @@ -22,9 +17,7 @@ pub struct StartCmd { impl StartCmd { async fn cmd(&self) -> Result<(), BoxError> { let config = app_config().clone(); - let light_config = LightConfig::load(LIGHT_CONFIG_PATH)?; - - start(config, light_config, self.reset).await + start(config, self.reset).await } } @@ -39,11 +32,11 @@ impl Runnable for StartCmd { } } -async fn start(config: Config, light_config: LightConfig, reset: bool) -> Result<(), BoxError> { +async fn start(config: Config, reset: bool) -> Result<(), BoxError> { let mut chains: Vec = vec![]; for chain_config in &config.chains { - let light_config = light_config.for_chain(&chain_config.id).ok_or_else(|| { + let light_config = chain_config.primary().ok_or_else(|| { format!( "could not find light client configuration for chain {}", chain_config.id diff --git a/relayer-cli/src/config.rs b/relayer-cli/src/config.rs index 1562a1a1c0..a47c7be070 100644 --- a/relayer-cli/src/config.rs +++ b/relayer-cli/src/config.rs @@ -4,4 +4,20 @@ //! application's configuration file and/or command-line options //! for specifying it. +use std::path::PathBuf; + +use abscissa_core::{error::BoxError, EntryPoint, Options}; + +use crate::commands::CliCmd; + pub use relayer::config::Config; + +/// Get the path to configuration file +pub fn config_path() -> Result { + let mut args = std::env::args(); + assert!(args.next().is_some(), "expected one argument but got zero"); + let args = args.collect::>(); + let app = EntryPoint::::parse_args_default(args.as_slice())?; + let config_path = app.config.ok_or_else(|| "no config file specified")?; + Ok(config_path) +} diff --git a/relayer-cli/src/lib.rs b/relayer-cli/src/lib.rs index c54e73b8e0..70f1ee7981 100644 --- a/relayer-cli/src/lib.rs +++ b/relayer-cli/src/lib.rs @@ -20,6 +20,5 @@ pub mod application; pub mod commands; pub mod config; pub mod error; -pub mod light; pub mod prelude; pub mod tasks; diff --git a/relayer-cli/src/light.rs b/relayer-cli/src/light.rs deleted file mode 100644 index 3069f40deb..0000000000 --- a/relayer-cli/src/light.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Datatypes relative to the light client commands - -pub mod config; diff --git a/relayer-cli/src/light/config.rs b/relayer-cli/src/light/config.rs deleted file mode 100644 index 2925c316e6..0000000000 --- a/relayer-cli/src/light/config.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Light client configuration - -use std::collections::HashMap; -use std::fs; -use std::path::Path; - -use serde_derive::{Deserialize, Serialize}; - -use relayer::client::TrustOptions; -use tendermint::chain; - -use crate::error; - -/// Default path for the light command config file -pub const LIGHT_CONFIG_PATH: &str = "light_config.toml"; - -/// Light client configuration, as set by the `light init` command. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LightConfig { - /// Mapping from chain identifier to trust options - pub chains: HashMap, -} - -impl LightConfig { - /// Load the configuration from a TOML file at `path`, or return the empty - /// config if the file does not exists. - pub fn load(path: impl AsRef) -> Result { - let config = match fs::read_to_string(path) { - Ok(contents) => { - toml::from_str(&contents).map_err(|e| error::Kind::Config.context(e))? - } - Err(_) => LightConfig { - chains: HashMap::new(), - }, - }; - - Ok(config) - } - - /// Save the configuration to a TOML file at `path` - pub fn save(&self, path: impl AsRef) -> Result<(), error::Error> { - let contents = toml::to_string_pretty(self).map_err(|e| error::Kind::Config.context(e))?; - fs::write(path, contents).map_err(|e| error::Kind::Config.context(e))?; - - Ok(()) - } - - /// Get the configuration for a specific chain - pub fn for_chain(&self, chain_id: &chain::Id) -> Option<&TrustOptions> { - self.chains.get(chain_id) - } -} diff --git a/relayer-cli/src/tasks/light_client.rs b/relayer-cli/src/tasks/light_client.rs index 121f7d86d8..8aff0e95da 100644 --- a/relayer-cli/src/tasks/light_client.rs +++ b/relayer-cli/src/tasks/light_client.rs @@ -18,7 +18,8 @@ use tendermint_light_client::{ use relayer::{ chain::{Chain, CosmosSDKChain}, - client::{self, TrustOptions}, + client, + config::LightClientConfig, }; use crate::prelude::*; @@ -34,7 +35,7 @@ use crate::prelude::*; /// otherwise it will resume from the latest trusted block in the store. pub async fn create( chain: &mut CosmosSDKChain, - trust_options: TrustOptions, + config: LightClientConfig, reset: bool, ) -> Result>, BoxError> { status_info!( @@ -43,7 +44,7 @@ pub async fn create( chain.config().id, ); - let supervisor = create_client(chain, trust_options, reset).await?; + let supervisor = create_client(chain, config, reset).await?; let handle = supervisor.handle(); let thread_handle = std::thread::spawn(|| supervisor.run()); let task = client_task(chain.id().clone(), handle.clone()); @@ -59,7 +60,7 @@ fn build_instance( chain: &CosmosSDKChain, store: store::sled::SledStore, options: light_client::Options, - trust_options: TrustOptions, + config: LightClientConfig, reset: bool, ) -> Result { let builder = LightClientBuilder::prod( @@ -72,7 +73,7 @@ fn build_instance( let builder = if reset { info!(chain.id = %chain.id(), "resetting client to trust options state"); - builder.trust_primary_at(trust_options.height, trust_options.header_hash)? + builder.trust_primary_at(config.trusted_height, config.trusted_header_hash)? } else { info!(chain.id = %chain.id(), "starting client from stored trusted state"); builder.trust_from_store()? @@ -83,7 +84,7 @@ fn build_instance( async fn create_client( chain: &mut CosmosSDKChain, - trust_options: TrustOptions, + config: LightClientConfig, reset: bool, ) -> Result { let chain_config = chain.config(); @@ -97,9 +98,9 @@ async fn create_client( let witness_peer_id: PeerId = "DC0C0ADEADBEEFC0FFEEFACADEBADFADAD0BEFEE".parse().unwrap(); let options = light_client::Options { - trust_threshold: trust_options.trust_threshold, - trusting_period: trust_options.trusting_period, - clock_drift: Duration::from_secs(5), // TODO: Make configurable + trust_threshold: chain_config.trust_threshold, + trusting_period: chain_config.trusting_period, + clock_drift: chain_config.clock_drift, }; let primary = build_instance( @@ -107,18 +108,11 @@ async fn create_client( &chain, store.clone(), options, - trust_options.clone(), + config.clone(), reset, )?; - let witness = build_instance( - witness_peer_id, - &chain, - store, - options, - trust_options, - reset, - )?; + let witness = build_instance(witness_peer_id, &chain, store, options, config, reset)?; let supervisor = SupervisorBuilder::new() .primary(primary_peer_id, chain_config.rpc_addr.clone(), primary) diff --git a/relayer-cli/tests/integration.rs b/relayer-cli/tests/integration.rs index a6416d6e61..4855f6eec0 100644 --- a/relayer-cli/tests/integration.rs +++ b/relayer-cli/tests/integration.rs @@ -16,7 +16,7 @@ use ibc::ics04_channel::channel::{ChannelEnd, Order, State as ChannelState}; use ibc::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; use ibc::ics24_host::Path::{ChannelEnds, ClientConnections}; use relayer::chain::{Chain, CosmosSDKChain}; -use relayer::config::{ChainConfig, Config}; +use relayer::config::{default, ChainConfig, Config}; use tendermint::net::Address; use tendermint_proto::DomainType; @@ -35,8 +35,10 @@ fn simd_config() -> Config { store_prefix: "ibc".to_string(), client_ids: vec!["ethbridge".to_string()], gas: 200000, - trusting_period: Default::default(), - peer_id: "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(), + trust_threshold: Default::default(), + trusting_period: default::trusting_period(), + clock_drift: default::clock_drift(), + peers: None, }]; config } diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 57b8b6c8e8..061c7b8e0b 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -78,9 +78,6 @@ pub trait Chain { /// Set a light client for this chain fn set_light_client(&mut self, light_client: Self::LightClient); - /// The trusting period configured for this chain - fn trusting_period(&self) -> Duration; - /// The unbonding period of this chain /// TODO - this is a GRPC query, needs to be implemented fn unbonding_period(&self) -> Duration; diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 94a2e1c4fe..4b11befc66 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -40,8 +40,12 @@ pub struct CosmosSDKChain { impl CosmosSDKChain { pub fn from_config(config: ChainConfig) -> Result { + let primary = config + .primary() + .ok_or_else(|| Kind::LightClient.context("no primary peer specified"))?; + let rpc_client = - HttpClient::new(config.rpc_addr.clone()).map_err(|e| Kind::Rpc.context(e))?; + HttpClient::new(primary.address.clone()).map_err(|e| Kind::Rpc.context(e))?; let key_store = KeyRing::init(StoreBackend::Memory); @@ -204,10 +208,6 @@ impl Chain for CosmosSDKChain { self.light_client.as_ref() } - fn trusting_period(&self) -> Duration { - self.config.trusting_period - } - fn trust_threshold(&self) -> TrustThreshold { TrustThreshold::default() } @@ -219,6 +219,10 @@ impl Chain for CosmosSDKChain { fn query_header_at_height(&self, height: Height) -> Result { let client = self.rpc_client(); + let primary = self + .config() + .primary() + .ok_or_else(|| Kind::LightClient.context("no primary peer configured"))?; let signed_header = fetch_signed_header(client, height)?; assert_eq!(height, signed_header.header.height); @@ -230,7 +234,7 @@ impl Chain for CosmosSDKChain { signed_header, validator_set, next_validator_set, - self.config().peer_id, + primary.peer_id, ); Ok(light_block) diff --git a/relayer/src/client.rs b/relayer/src/client.rs index 15ad6fbd9f..4af3ddd2a7 100644 --- a/relayer/src/client.rs +++ b/relayer/src/client.rs @@ -11,9 +11,7 @@ use tendermint_light_client::types::{LightBlock, TrustThreshold}; use crate::chain; use crate::error; -pub mod trust_options; use ::tendermint::block::Height; -pub use trust_options::TrustOptions; pub mod tendermint; diff --git a/relayer/src/client/trust_options.rs b/relayer/src/client/trust_options.rs deleted file mode 100644 index a891bd2dfa..0000000000 --- a/relayer/src/client/trust_options.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::time::Duration; - -use anomaly::fail; -use serde_derive::{Deserialize, Serialize}; - -use tendermint::block::Height; -use tendermint::Hash; -use tendermint_light_client::types::TrustThreshold; - -use crate::error; - -/// The trust options for a `Client` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrustOptions { - pub header_hash: Hash, - pub height: Height, - pub trusting_period: Duration, - pub trust_threshold: TrustThreshold, -} - -impl TrustOptions { - pub fn new( - header_hash: Hash, - height: Height, - trusting_period: Duration, - trust_threshold: TrustThreshold, - ) -> Result { - if trusting_period <= Duration::new(0, 0) { - fail!( - error::Kind::LightClient, - "trusting period must be greater than zero" - ) - } - - Ok(Self { - header_hash, - height, - trusting_period, - trust_threshold, - }) - } -} diff --git a/relayer/src/config.rs b/relayer/src/config.rs index c90069acae..5d6f526ae4 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -1,17 +1,22 @@ //! Read the relayer configuration into the Config struct, in examples for now //! to support ADR validation..should move to relayer/src soon -use std::path::Path; -use std::time::Duration; +use std::{ + fs::File, + io::Write, + path::{Path, PathBuf}, + time::Duration, +}; use serde_derive::{Deserialize, Serialize}; -use tendermint::{chain, net, node}; -use crate::client::TrustOptions; +use tendermint::{chain, net, node, Hash}; +use tendermint_light_client::types::{Height, PeerId, TrustThreshold}; + use crate::error; /// Defaults for various fields -mod default { +pub mod default { use super::*; pub fn timeout() -> Duration { @@ -29,12 +34,18 @@ mod default { pub fn trusting_period() -> Duration { Duration::from_secs(336 * 60 * 60) // 336 hours } + + pub fn clock_drift() -> Duration { + Duration::from_secs(5) // 5 seconds + } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Config { pub global: GlobalConfig, + #[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")] pub chains: Vec, + #[serde(skip_serializing_if = "Option::is_none")] pub connections: Option>, // use all for default } @@ -78,11 +89,28 @@ pub struct ChainConfig { pub client_ids: Vec, #[serde(default = "default::gas")] pub gas: u64, + #[serde(default = "default::clock_drift", with = "humantime_serde")] + pub clock_drift: Duration, #[serde(default = "default::trusting_period", with = "humantime_serde")] pub trusting_period: Duration, + #[serde(default)] + pub trust_threshold: TrustThreshold, - // TODO: Move the light client config - pub peer_id: node::Id, + // initially empty, to configure with the `light add/rm` commands + #[serde(skip_serializing_if = "Option::is_none")] + pub peers: Option, +} + +impl ChainConfig { + pub fn primary(&self) -> Option<&LightClientConfig> { + let peers = self.peers.as_ref()?; + peers.light_client(peers.primary) + } + + pub fn light_client(&self, id: PeerId) -> Option<&LightClientConfig> { + let peers = self.peers.as_ref()?; + peers.light_client(id) + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -123,10 +151,31 @@ pub struct RelayPath { pub direction: Direction, // default bidirectional } -/// Attempt to load and parse the config file into the Config struct. -/// -/// TODO: If a file cannot be found, return a default Config allowing relayer -/// to relay everything from any to any chain(?) +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PeersConfig { + pub primary: PeerId, + pub light_clients: Vec, +} + +impl PeersConfig { + pub fn light_client(&self, id: PeerId) -> Option<&LightClientConfig> { + self.light_clients.iter().find(|p| p.peer_id == id) + } + + pub fn light_client_mut(&mut self, id: PeerId) -> Option<&mut LightClientConfig> { + self.light_clients.iter_mut().find(|p| p.peer_id == id) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LightClientConfig { + pub peer_id: PeerId, + pub address: net::Address, + pub trusted_header_hash: Hash, + pub trusted_height: Height, +} + +/// Attempt to load and parse the TOML config file as a `Config`. pub fn parse(path: impl AsRef) -> Result { let config_toml = std::fs::read_to_string(&path).map_err(|e| error::Kind::ConfigIo.context(e))?; @@ -137,9 +186,25 @@ pub fn parse(path: impl AsRef) -> Result { Ok(config) } +/// Serialize the given `Config` as TOML to the given config file. +pub fn store(config: &Config, path: impl AsRef) -> Result<(), error::Error> { + let mut file = File::create(path).map_err(|e| error::Kind::Config.context(e))?; + store_writer(config, &mut file) +} + +/// Serialize the given `Config` as TOML to the given writer. +pub(crate) fn store_writer(config: &Config, mut writer: impl Write) -> Result<(), error::Error> { + let toml_config = + toml::to_string_pretty(&config).map_err(|e| error::Kind::Config.context(e))?; + + writeln!(writer, "{}", toml_config).map_err(|e| error::Kind::Config.context(e))?; + + Ok(()) +} + #[cfg(test)] mod tests { - use super::parse; + use super::{parse, store_writer}; #[test] fn parse_valid_config() { @@ -152,4 +217,18 @@ mod tests { println!("{:?}", config); assert!(config.is_ok()); } + + #[test] + fn serialize_valid_config() { + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/config/fixtures/relayer_conf_example.toml" + ); + + let config = parse(path).expect("could not parse config"); + let mut buffer = Vec::new(); + + let result = store_writer(&config, &mut buffer); + assert!(result.is_ok()); + } } diff --git a/relayer/src/tx/client.rs b/relayer/src/tx/client.rs index 8e2f353ef2..12194e2547 100644 --- a/relayer/src/tx/client.rs +++ b/relayer/src/tx/client.rs @@ -74,7 +74,7 @@ pub fn create_client(opts: CreateClientOptions) -> Result, Error> { // Build the client state. let any_client_state = ibc::ics07_tendermint::client_state::ClientState::new( src_chain.id().to_string(), - src_chain.trusting_period(), + src_chain.config().trusting_period, src_chain.unbonding_period(), Duration::from_millis(3000), Height::new(ChainId::chain_version(version.clone()), height), diff --git a/relayer/tests/config/fixtures/relayer_conf_example.toml b/relayer/tests/config/fixtures/relayer_conf_example.toml index ab381f976a..c71cfd953c 100644 --- a/relayer/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/tests/config/fixtures/relayer_conf_example.toml @@ -1,64 +1,65 @@ -#This example config can be used between two chains chain_A and chain_B described in the repository README.md -title = "IBC Relayer Config Example" - [global] -timeout = "10s" -strategy = "naive" +timeout = '10s' +strategy = 'naive' [[chains]] - id = "chain_A" - rpc_addr = "localhost:26657" - account_prefix = "cosmos" - key_name = "testkey" - store_prefix = "ibc" - client_ids = ["clA1", "clA2"] - gas = 200000 - gas_adjustement = 1.3 - gas_price = "0.025stake" - trusting_period = "336h" +id = 'chain_A' +rpc_addr = 'tcp://localhost:26657' +account_prefix = 'cosmos' +key_name = 'testkey' +store_prefix = 'ibc' +client_ids = [ + 'clA1', + 'clA2', +] +gas = 200000 +clock_drift = '5s' +trusting_period = '14days' - # TODO: Move to light client config - peer_id = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE" +[chains.trust_threshold] +numerator = '1' +denominator = '3' [[chains]] - id = "chain_B" - rpc_addr = "localhost:26557" - account_prefix = "cosmos" - key_name = "testkey" - store_prefix = "ibc" - client_ids = ["clB1"] - gas = 200000 - gas_adjustement = 1.3 - gas_price = "0.025stake" - trusting_period = "336h" +id = 'chain_B' +rpc_addr = 'tcp://localhost:26557' +account_prefix = 'cosmos' +key_name = 'testkey' +store_prefix = 'ibc' +client_ids = ['clB1'] +gas = 200000 +clock_drift = '5s' +trusting_period = '14days' - # TODO: Move to light client config - peer_id = "DC0C0ADEADBEEFC0FFEEFACADEBADFADAD0BEFEE" +[chains.trust_threshold] +numerator = '1' +denominator = '3' [[connections]] - [connections.src] - chain_id = "chain_A" - client_id = "clB1" - connection_id = "connAtoB" +chain_id = 'chain_A' +client_id = 'clB1' +connection_id = 'connAtoB' [connections.dest] - chain_id = "chain_B" - client_id = "clA1" - connection_id = "connBtoA" +chain_id = 'chain_B' +client_id = 'clA1' +connection_id = 'connBtoA' [[connections.paths]] - src_port = "portA1" - dest_port = "portB1" - direction = "unidirectional" +src_port = 'portA1' +dest_port = 'portB1' +direction = 'unidirectional' [[connections.paths]] - src_port = "portA2" - dest_port = "portB2" - direction = "bidirectional" +src_port = 'portA2' +dest_port = 'portB2' +direction = 'bidirectional' [[connections.paths]] - src_port = "portA3" - dest_port = "portB3" - src_channel = "chan3onA" - dest_channel = "chan3onB" +src_port = 'portA3' +dest_port = 'portB3' +src_channel = 'chan3onA' +dest_channel = 'chan3onB' +direction = 'bidirectional' +