diff --git a/.changelog/unreleased/features/ibc-relayer/1518-config-modes.md b/.changelog/unreleased/features/ibc-relayer/1518-config-modes.md new file mode 100644 index 0000000000..c42e7de9b0 --- /dev/null +++ b/.changelog/unreleased/features/ibc-relayer/1518-config-modes.md @@ -0,0 +1,2 @@ +- Allow for more granular control of relaying modes. The `mode` configuration section replaces the `strategy` option. + ([#1518](https://github.com/informalsystems/ibc-rs/issues/1518)) diff --git a/ci/simple_config.toml b/ci/simple_config.toml index fd4736f55f..9e23366d5e 100644 --- a/ci/simple_config.toml +++ b/ci/simple_config.toml @@ -1,8 +1,25 @@ [global] -strategy = 'all' log_level = 'trace' + +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true filter = false -clear_packets_interval = 100 +tx_confirmation = true [telemetry] enabled = false diff --git a/config.toml b/config.toml index 13ddfd859e..a2502e438a 100644 --- a/config.toml +++ b/config.toml @@ -1,13 +1,45 @@ # The global section has parameters that apply globally to the relayer operation. [global] -# Specify the strategy to be used by the relayer. Default: 'packets' -# Two options are currently supported: -# - 'all': Relay packets and perform channel and connection handshakes. -# - 'packets': Relay packets only. -strategy = 'packets' +# Specify the verbosity for the relayer logging output. Default: 'info' +# Valid options are 'error', 'warn', 'info', 'debug', 'trace'. +log_level = 'info' + -# Enable or disable the filtering mechanism. Default: 'false' +# Specify the mode to be used by the relayer. [Required] +[mode] + +# Specify the client mode. +[mode.clients] +# Whether or not to enable the client workers. [Required] +enabled = false +# Whether or not to enable periodic refresh of clients. [Default: false] +refresh = true +# Whether or not to enable misbehaviour detection for clients. [Default: false] +misbehaviour = true + +# Specify the connections mode. +[mode.connections] +# Whether or not to enable the connection workers. [Required] +enabled = false + +# Specify the channels mode. +[mode.channels] +# Whether or not to enable the channel workers. [Required] +enabled = false + +# Specify the packets mode. +[mode.packets] +# Whether or not to enable the packet workers. [Required] +enabled = false +# Parametrize the periodic packet clearing feature. +# Interval (in number of blocks) at which pending packets +# should be eagerly cleared. A value of '0' will disable +# periodic packet clearing. [Default: 100] +clear_interval = 100 +# Whether or not to clear packets on start. [Default: false] +clear_on_start = true +# Enable or disable the filtering mechanism. # Valid options are 'true', 'false'. # Currently Hermes supports two filters: # 1. Packet filtering on a per-chain basis; see the chain-specific @@ -16,24 +48,14 @@ strategy = 'packets' # is parametrized with (numerator = 1, denominator = 3), so that clients with # thresholds different than this will be ignored. # If set to 'true', both of the above filters will be enabled. +# [Default: false] filter = false - -# Specify the verbosity for the relayer logging output. Default: 'info' -# Valid options are 'error', 'warn', 'info', 'debug', 'trace'. -log_level = 'info' - -# Parametrize the periodic packet clearing feature. -# Interval (in number of blocks) at which pending packets -# should be eagerly cleared. A value of '0' will disable -# periodic packet clearing. Default: 100 -clear_packets_interval = 100 - # Toggle the transaction confirmation mechanism. # The tx confirmation mechanism periodically queries the `/tx_search` RPC # endpoint to check that previously-submitted transactions # (to any chain in this config file) have delivered successfully. # Experimental feature. Affects telemetry if set to false. -# Default: true. +# [Default: true] tx_confirmation = true diff --git a/docs/architecture/adr-002-ibc-relayer.md b/docs/architecture/adr-002-ibc-relayer.md index e2aa1cec98..cb80078628 100644 --- a/docs/architecture/adr-002-ibc-relayer.md +++ b/docs/architecture/adr-002-ibc-relayer.md @@ -117,9 +117,28 @@ Below is an example of a configuration file. ```toml [global] -strategy = "packets" log_level = "error" +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true +filter = false +tx_confirmation = true + [[chains]] id = "chain_A" rpc_addr = "http://localhost:26657" @@ -172,14 +191,7 @@ pub struct Config { pub connections: Option>, } -pub enum Strategy { - Packets, - HandshakeAndPackets, -} - pub struct GlobalConfig { - pub strategy: Strategy, - /// All valid log levels, as defined in tracing: /// https://docs.rs/tracing-core/0.1.17/tracing_core/struct.Level.html pub log_level: String, diff --git a/docs/architecture/adr-006-hermes-v0.2-usecases.md b/docs/architecture/adr-006-hermes-v0.2-usecases.md index 7a9b361f65..bbbabc5596 100644 --- a/docs/architecture/adr-006-hermes-v0.2-usecases.md +++ b/docs/architecture/adr-006-hermes-v0.2-usecases.md @@ -252,7 +252,6 @@ of the config file will look as follows: ```toml [global] -strategy = 'packets' log_level = 'error' log_json = 'false' ``` diff --git a/e2e/e2e/channel.py b/e2e/e2e/channel.py index 960d20695b..90921aff7b 100644 --- a/e2e/e2e/channel.py +++ b/e2e/e2e/channel.py @@ -553,10 +553,14 @@ def verify_state(c: Config, ibc1: ChainId, ibc0: ChainId, ibc1_chan_id: ChannelId, port_id: PortId): - strategy = toml.load(c.config_file)['global']['strategy'] - # verify channel state on both chains, should be 'Open' for 'all' strategy, 'Init' otherwise - - if strategy == 'all': + mode = toml.load(c.config_file)['mode'] + clients_enabled = mode['clients']['enabled'] + conn_enabled = mode['connections']['enabled'] + chan_enabled = mode['channels']['enabled'] + packets_enabled = mode['packets']['enabled'] + + # verify connection state on both chains, should be 'Open' or 'Init' depending on config 'mode' + if clients_enabled and conn_enabled and chan_enabled and packets_enabled: sleep(10.0) for i in range(20): sleep(2.0) @@ -569,7 +573,7 @@ def verify_state(c: Config, assert (ibc0_chan_end.state == 'Open'), (ibc0_chan_end, "state is not Open") assert (ibc1_chan_end.state == 'Open'), (ibc1_chan_end, "state is not Open") - elif strategy == 'packets': + else: sleep(5.0) ibc1_chan_end = query_channel_end(c, ibc1, port_id, ibc1_chan_id) assert (ibc1_chan_end.state == 'Init'), (ibc1_chan_end, "state is not Init") diff --git a/e2e/e2e/connection.py b/e2e/e2e/connection.py index 5ebb113824..876f2917d3 100644 --- a/e2e/e2e/connection.py +++ b/e2e/e2e/connection.py @@ -263,11 +263,14 @@ def verify_state(c: Config, ibc1: ChainId, ibc0: ChainId, ibc1_conn_id: ConnectionId): - strategy = toml.load(c.config_file)['global']['strategy'] - l.debug(f'Using strategy: {strategy}') - - # verify connection state on both chains, should be 'Open' for 'all' strategy, 'Init' otherwise - if strategy == 'all': + mode = toml.load(c.config_file)['mode'] + clients_enabled = mode['clients']['enabled'] + conn_enabled = mode['connections']['enabled'] + chan_enabled = mode['channels']['enabled'] + packets_enabled = mode['packets']['enabled'] + + # verify connection state on both chains, should be 'Open' or 'Init' depending on config 'mode' + if clients_enabled and conn_enabled and chan_enabled and packets_enabled: sleep(10.0) for i in range(20): sleep(5.0) @@ -280,7 +283,7 @@ def verify_state(c: Config, assert (ibc0_conn_end.state == 'Open'), (ibc0_conn_end, "state is not Open") assert (ibc1_conn_end.state == 'Open'), (ibc1_conn_end, "state is not Open") - elif strategy == 'packets': + else: sleep(5.0) ibc1_conn_end = query_connection_end(c, ibc1, ibc1_conn_id) assert (ibc1_conn_end.state == 'Init'), (ibc1_conn_end, "state is not Init") diff --git a/guide/src/commands/relaying/handshakes.md b/guide/src/commands/relaying/handshakes.md index e99f3bf0bc..a504cffafd 100644 --- a/guide/src/commands/relaying/handshakes.md +++ b/guide/src/commands/relaying/handshakes.md @@ -5,11 +5,26 @@ for connections and channels. ## The `start` Command -To relay packets and handshake messages use `all` as strategy in the `global` section of the configuration file: +To relay packets and handshake messages configure the `mode` section of the configuration file like so: ```toml [global] -strategy = 'all' log_level = 'info' + +[mode] + +[mode.clients] +enabled = true +# ... + +[mode.connections] +enabled = true + +[mode.channels] +enabled = true + +[mode.packets] +enabled = true +# ... ``` Then start hermes using the start command: @@ -28,15 +43,15 @@ the configured chains. Assuming the events are coming from a `source` chain, the relayer determines the `destination` chain and builds the handshake messages based on these events. These are then sent to the `destination` chain. -In addition to the events described in [Packet Relaying](packets.md#packet-relaying), in the `all` strategy mode the following IBC events are handled: +In addition to the events described in [Packet Relaying](packets.md#packet-relaying), the following IBC events may be handled: -- Channels: +- Channels (if `mode.channels.enabled=true`): - `chan_open_init`: the relayer builds a `MsgChannelOpenTry` message - `chan_open_try`: the relayer builds a `MsgChannelOpenAck` message - `chan_open_ack`: the relayer builds a `MsgChannelOpenConfirm` message - `chan_open_confirm`: no message is sent out, channel opening is finished -- Connections: +- Connections (if `mode.connections.enabled=true`): - `conn_open_init`: the relayer builds a `MsgConnOpenTry` message - `conn_open_try`: the relayer builds a `MsgConnOpenAck` message - `conn_open_ack`: the relayer builds a `MsgConnOpenConfirm` message diff --git a/guide/src/commands/relaying/packets.md b/guide/src/commands/relaying/packets.md index 8ee0a6925c..88644dcafe 100644 --- a/guide/src/commands/relaying/packets.md +++ b/guide/src/commands/relaying/packets.md @@ -9,11 +9,26 @@ This section describes the configuration and commands that can be used to start ## The `start` Command -To relay packets only use `packets` as strategy in the `global` section of the configuration file: +To relay packets only configure the `mode` section of the configuration file like so: ```toml [global] -strategy = 'packets' log_level = 'info' + +[mode] + +[mode.clients] +enabled = true +# ... + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +# ... ``` Then start hermes using the start command: diff --git a/guide/src/config.md b/guide/src/config.md index 75fdc6e8f5..7cb72fdc7d 100644 --- a/guide/src/config.md +++ b/guide/src/config.md @@ -49,10 +49,27 @@ Here is a full example of a configuration file with two chains configured: ```toml [global] -strategy = 'all' log_level = 'info' + +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true filter = false -clear_packets_interval = 100 +tx_confirmation = true [rest] enabled = true diff --git a/guide/src/help.md b/guide/src/help.md index 93f2426c58..7147969495 100644 --- a/guide/src/help.md +++ b/guide/src/help.md @@ -89,7 +89,6 @@ Relevant snippet: ```toml [global] -strategy = 'packets' log_level = 'error' ``` diff --git a/guide/src/tutorials/local-chains/relay-paths/multiple-paths.md b/guide/src/tutorials/local-chains/relay-paths/multiple-paths.md index 0c0839e1c2..1810d8fdfd 100644 --- a/guide/src/tutorials/local-chains/relay-paths/multiple-paths.md +++ b/guide/src/tutorials/local-chains/relay-paths/multiple-paths.md @@ -8,8 +8,27 @@ Follow the steps below to connect three chains together and relay packets betwee ```toml [global] - strategy = 'packets' log_level = 'info' + + [mode] + + [mode.clients] + enabled = true + refresh = true + misbehaviour = true + + [mode.connections] + enabled = false + + [mode.channels] + enabled = false + + [mode.packets] + enabled = true + clear_interval = 100 + clear_on_start = true + filter = false + tx_confirmation = true [[chains]] id = 'ibc-0' diff --git a/relayer-cli/src/application.rs b/relayer-cli/src/application.rs index 96b5121a7a..6c16de3bde 100644 --- a/relayer-cli/src/application.rs +++ b/relayer-cli/src/application.rs @@ -123,12 +123,21 @@ impl Application for CliApp { /// time in app lifecycle when configuration would be loaded if /// possible. fn after_config(&mut self, config: Self::Cfg) -> Result<(), FrameworkError> { + use crate::config::Diagnostic; + // Configure components self.state.components.after_config(&config)?; - validate_config(&config).map_err(|validation_err| { - FrameworkErrorKind::ConfigError.context(format!("{}", validation_err)) - })?; + if let Err(diagnostic) = validate_config(&config) { + match diagnostic { + Diagnostic::Warning(e) => { + tracing::warn!("relayer may be misconfigured: {}", e); + } + Diagnostic::Error(e) => { + return Err(FrameworkErrorKind::ConfigError.context(e).into()); + } + } + }; self.config = Some(config); diff --git a/relayer-cli/src/commands/config/validate.rs b/relayer-cli/src/commands/config/validate.rs index 79c2fd1939..9cf61b9236 100644 --- a/relayer-cli/src/commands/config/validate.rs +++ b/relayer-cli/src/commands/config/validate.rs @@ -13,9 +13,11 @@ impl Runnable for ValidateCmd { let config = app_config(); trace!("loaded configuration: {:#?}", *config); + // No need to output the underlying error, this is done already when the application boots. + // See `application::CliApp::after_config`. match config::validate_config(&config) { - Ok(_) => Output::success("validation passed successfully").exit(), - Err(e) => Output::error(format!("{}", e)).exit(), + Ok(_) => Output::success("configuration is valid").exit(), + Err(_) => Output::error("configuration is invalid").exit(), } } } diff --git a/relayer-cli/src/config.rs b/relayer-cli/src/config.rs index 40481ca3bc..ff48da2c8e 100644 --- a/relayer-cli/src/config.rs +++ b/relayer-cli/src/config.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use flex_error::{define_error, TraceError}; use ibc::core::ics24_host::identifier::ChainId; -use ibc_relayer::config::Config; +use ibc_relayer::config::{Config, ModeConfig}; use tendermint_light_client::types::TrustThreshold; use tracing_subscriber::filter::ParseError; @@ -36,6 +36,13 @@ define_error! { e.log_level) }, + InvalidMode + { reason: String, } + |e| { + format!("config file specifies invalid mode config, caused by: {0}", + e.reason) + }, + DuplicateChains { chain_id: ChainId } |e| { @@ -56,18 +63,44 @@ define_error! { } } +#[derive(Clone, Debug)] +pub enum Diagnostic { + Warning(E), + Error(E), +} + /// Method for syntactic validation of the input configuration file. -pub fn validate_config(config: &Config) -> Result<(), Error> { +pub fn validate_config(config: &Config) -> Result<(), Diagnostic> { // Check for duplicate chain configuration and invalid trust thresholds let mut unique_chain_ids = BTreeSet::new(); for c in config.chains.iter() { - if !unique_chain_ids.insert(c.id.clone()) { - return Err(Error::duplicate_chains(c.id.clone())); + let already_present = !unique_chain_ids.insert(c.id.clone()); + if already_present { + return Err(Diagnostic::Error(Error::duplicate_chains(c.id.clone()))); } validate_trust_threshold(&c.id, c.trust_threshold)?; } + // Check for invalid mode config + validate_mode(&config.mode)?; + + Ok(()) +} + +fn validate_mode(mode: &ModeConfig) -> Result<(), Diagnostic> { + if mode.all_disabled() { + return Err(Diagnostic::Warning(Error::invalid_mode( + "all operation modes of Hermes are disabled, relayer won't perform any action aside from subscribing to events".to_string(), + ))); + } + + if mode.clients.enabled && !mode.clients.refresh && !mode.clients.misbehaviour { + return Err(Diagnostic::Error(Error::invalid_mode( + "either `refresh` or `misbehaviour` must be set to true if `clients.enabled` is set to true".to_string(), + ))); + } + Ok(()) } @@ -76,29 +109,32 @@ pub fn validate_config(config: &Config) -> Result<(), Error> { /// a) non-zero /// b) greater or equal to 1/3 /// c) strictly less than 1 -fn validate_trust_threshold(id: &ChainId, trust_threshold: TrustThreshold) -> Result<(), Error> { +fn validate_trust_threshold( + id: &ChainId, + trust_threshold: TrustThreshold, +) -> Result<(), Diagnostic> { if trust_threshold.denominator() == 0 { - return Err(Error::invalid_trust_threshold( + return Err(Diagnostic::Error(Error::invalid_trust_threshold( trust_threshold, id.clone(), "trust threshold denominator cannot be zero".to_string(), - )); + ))); } if trust_threshold.numerator() * 3 < trust_threshold.denominator() { - return Err(Error::invalid_trust_threshold( + return Err(Diagnostic::Error(Error::invalid_trust_threshold( trust_threshold, id.clone(), "trust threshold cannot be < 1/3".to_string(), - )); + ))); } if trust_threshold.numerator() >= trust_threshold.denominator() { - return Err(Error::invalid_trust_threshold( + return Err(Diagnostic::Error(Error::invalid_trust_threshold( trust_threshold, id.clone(), "trust threshold cannot be >= 1".to_string(), - )); + ))); } Ok(()) diff --git a/relayer-cli/tests/fixtures/two_chains.toml b/relayer-cli/tests/fixtures/two_chains.toml index 2c04d671c8..92a34cf422 100644 --- a/relayer-cli/tests/fixtures/two_chains.toml +++ b/relayer-cli/tests/fixtures/two_chains.toml @@ -1,7 +1,26 @@ [global] -strategy = 'naive' log_level = 'error' # valid options: 'error', 'warn', 'info', 'debug', 'trace' +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true +filter = false +tx_confirmation = true + [[chains]] id = 'ibc-0' rpc_addr = 'http://127.0.0.1:26657' diff --git a/relayer/src/config.rs b/relayer/src/config.rs index 2ce0912bbe..32e6ae2a3d 100644 --- a/relayer/src/config.rs +++ b/relayer/src/config.rs @@ -116,6 +116,8 @@ pub struct Config { #[serde(default)] pub global: GlobalConfig, #[serde(default)] + pub mode: ModeConfig, + #[serde(default)] pub rest: RestConfig, #[serde(default)] pub telemetry: TelemetryConfig, @@ -145,7 +147,7 @@ impl Config { port_id: &PortId, channel_id: &ChannelId, ) -> bool { - if !self.global.filter { + if !self.mode.packets.filter { return true; } @@ -155,27 +157,74 @@ impl Config { } } - pub fn handshake_enabled(&self) -> bool { - self.global.strategy == Strategy::HandshakeAndPackets - } - pub fn chains_map(&self) -> HashMap<&ChainId, &ChainConfig> { self.chains.iter().map(|c| (&c.id, c)).collect() } } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub enum Strategy { - #[serde(rename = "packets")] - Packets, +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ModeConfig { + pub clients: Clients, + pub connections: Connections, + pub channels: Channels, + pub packets: Packets, +} + +impl ModeConfig { + pub fn all_disabled(&self) -> bool { + !self.clients.enabled + && !self.connections.enabled + && !self.channels.enabled + && !self.packets.enabled + } +} + +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Clients { + pub enabled: bool, + #[serde(default)] + pub refresh: bool, + #[serde(default)] + pub misbehaviour: bool, +} - #[serde(rename = "all")] - HandshakeAndPackets, +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Connections { + pub enabled: bool, } -impl Default for Strategy { +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Channels { + pub enabled: bool, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Packets { + pub enabled: bool, + #[serde(default = "default::clear_packets_interval")] + pub clear_interval: u64, + #[serde(default)] + pub clear_on_start: bool, + #[serde(default = "default::filter")] + pub filter: bool, + #[serde(default = "default::tx_confirmation")] + pub tx_confirmation: bool, +} + +impl Default for Packets { fn default() -> Self { - Self::Packets + Self { + enabled: false, + clear_interval: default::clear_packets_interval(), + clear_on_start: false, + filter: default::filter(), + tx_confirmation: default::tx_confirmation(), + } } } @@ -213,24 +262,13 @@ impl fmt::Display for LogLevel { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(default, deny_unknown_fields)] pub struct GlobalConfig { - pub strategy: Strategy, pub log_level: LogLevel, - #[serde(default = "default::filter")] - pub filter: bool, - #[serde(default = "default::clear_packets_interval")] - pub clear_packets_interval: u64, - #[serde(default = "default::tx_confirmation")] - pub tx_confirmation: bool, } impl Default for GlobalConfig { fn default() -> Self { Self { - strategy: Strategy::default(), log_level: LogLevel::default(), - filter: default::filter(), - clear_packets_interval: default::clear_packets_interval(), - tx_confirmation: default::tx_confirmation(), } } } diff --git a/relayer/src/supervisor.rs b/relayer/src/supervisor.rs index 1f881386cc..8e4db3ed41 100644 --- a/relayer/src/supervisor.rs +++ b/relayer/src/supervisor.rs @@ -93,14 +93,24 @@ impl Supervisor { /// Returns `false` otherwise. fn client_filter_enabled(&self) -> bool { // Currently just a wrapper over the global filter. - self.config.read().expect("poisoned lock").global.filter + self.config + .read() + .expect("poisoned lock") + .mode + .packets + .filter } /// Returns `true` if the relayer should filter based on /// channel identifiers. /// Returns `false` otherwise. fn channel_filter_enabled(&self) -> bool { - self.config.read().expect("poisoned lock").global.filter + self.config + .read() + .expect("poisoned lock") + .mode + .packets + .filter } fn relay_packets_on_channel( @@ -171,6 +181,28 @@ impl Supervisor { } } + /// If `enabled`, build an `Object` using the provided `object_ctor` + /// and add the given `event` to the `collected` events for this `object`. + fn collect_event( + &self, + collected: &mut CollectedEvents, + event: &IbcEvent, + enabled: bool, + object_ctor: F, + ) where + F: FnOnce() -> Option, + { + if enabled { + if let Some(object) = object_ctor() { + collected + .per_object + .entry(object) + .or_default() + .push(event.clone()); + } + } + } + /// Collect the events we are interested in from an [`EventBatch`], /// and maps each [`IbcEvent`] to their corresponding [`Object`]. pub fn collect_events( @@ -180,11 +212,7 @@ impl Supervisor { ) -> CollectedEvents { let mut collected = CollectedEvents::new(batch.height, batch.chain_id.clone()); - let handshake_enabled = self - .config - .read() - .expect("poisoned lock") - .handshake_enabled(); + let mode = self.config.read().expect("poisoned lock").mode; for event in &batch.events { match event { @@ -192,144 +220,81 @@ impl Supervisor { collected.new_block = Some(event.clone()); } IbcEvent::UpdateClient(ref update) => { - if let Ok(object) = Object::for_update_client(update, src_chain) { + self.collect_event(&mut collected, event, mode.clients.enabled, || { // Collect update client events only if the worker exists - if self.workers.contains(&object) { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); + if let Ok(object) = Object::for_update_client(update, src_chain) { + self.workers.contains(&object).then(|| object) + } else { + None } - } + }); } IbcEvent::OpenInitConnection(..) | IbcEvent::OpenTryConnection(..) | IbcEvent::OpenAckConnection(..) => { - if !handshake_enabled { - continue; - } - - let object = event - .connection_attributes() - .map(|attr| Object::connection_from_conn_open_events(attr, src_chain)); - - if let Some(Ok(object)) = object { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.connections.enabled, || { + event + .connection_attributes() + .map(|attr| { + Object::connection_from_conn_open_events(attr, src_chain).ok() + }) + .flatten() + }); } IbcEvent::OpenInitChannel(..) | IbcEvent::OpenTryChannel(..) => { - if !handshake_enabled { - continue; - } - - let object = event - .channel_attributes() - .map(|attr| Object::channel_from_chan_open_events(attr, src_chain)); - - if let Some(Ok(object)) = object { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.channels.enabled, || { + event + .channel_attributes() + .map(|attr| Object::channel_from_chan_open_events(attr, src_chain).ok()) + .flatten() + }); } IbcEvent::OpenAckChannel(ref open_ack) => { // Create client and packet workers here as channel end must be opened - if let Ok(client_object) = - Object::client_from_chan_open_events(open_ack.attributes(), src_chain) - { - collected - .per_object - .entry(client_object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.clients.enabled, || { + Object::client_from_chan_open_events(open_ack.attributes(), src_chain).ok() + }); - if let Ok(packet_object) = - Object::packet_from_chan_open_events(open_ack.attributes(), src_chain) - { - collected - .per_object - .entry(packet_object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.packets.enabled, || { + Object::packet_from_chan_open_events(open_ack.attributes(), src_chain).ok() + }); // If handshake message relaying is enabled create worker to send the MsgChannelOpenConfirm message - if handshake_enabled { - if let Ok(channel_object) = - Object::channel_from_chan_open_events(open_ack.attributes(), src_chain) - { - collected - .per_object - .entry(channel_object) - .or_default() - .push(event.clone()); - } - } + self.collect_event(&mut collected, event, mode.channels.enabled, || { + Object::channel_from_chan_open_events(open_ack.attributes(), src_chain).ok() + }); } IbcEvent::OpenConfirmChannel(ref open_confirm) => { // Create client worker here as channel end must be opened - if let Ok(client_object) = + self.collect_event(&mut collected, event, mode.clients.enabled, || { Object::client_from_chan_open_events(open_confirm.attributes(), src_chain) - { - collected - .per_object - .entry(client_object) - .or_default() - .push(event.clone()); - } - if let Ok(packet_object) = + .ok() + }); + + self.collect_event(&mut collected, event, mode.packets.enabled, || { Object::packet_from_chan_open_events(open_confirm.attributes(), src_chain) - { - collected - .per_object - .entry(packet_object) - .or_default() - .push(event.clone()); - } + .ok() + }); } IbcEvent::SendPacket(ref packet) => { - if let Ok(object) = Object::for_send_packet(packet, src_chain) { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.packets.enabled, || { + Object::for_send_packet(packet, src_chain).ok() + }); } IbcEvent::TimeoutPacket(ref packet) => { - if let Ok(object) = Object::for_timeout_packet(packet, src_chain) { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.packets.enabled, || { + Object::for_timeout_packet(packet, src_chain).ok() + }); } IbcEvent::WriteAcknowledgement(ref packet) => { - if let Ok(object) = Object::for_write_ack(packet, src_chain) { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.packets.enabled, || { + Object::for_write_ack(packet, src_chain).ok() + }); } IbcEvent::CloseInitChannel(ref packet) => { - if let Ok(object) = Object::for_close_init_channel(packet, src_chain) { - collected - .per_object - .entry(object) - .or_default() - .push(event.clone()); - } + self.collect_event(&mut collected, event, mode.packets.enabled, || { + Object::for_close_init_channel(packet, src_chain).ok() + }); } _ => (), } diff --git a/relayer/src/supervisor/spawn.rs b/relayer/src/supervisor/spawn.rs index f3a70dcee1..dff4ebd4e9 100644 --- a/relayer/src/supervisor/spawn.rs +++ b/relayer/src/supervisor/spawn.rs @@ -66,7 +66,12 @@ impl<'a, Chain: ChainHandle + 'static> SpawnContext<'a, Chain> { fn client_filter_enabled(&self) -> bool { // Currently just a wrapper over the global filter. - self.config.read().expect("poisoned lock").global.filter + self.config + .read() + .expect("poisoned lock") + .mode + .packets + .filter } pub fn spawn_workers(&mut self) { @@ -393,11 +398,13 @@ impl<'a, Chain: ChainHandle + 'static> SpawnContext<'a, Chain> { client: IdentifiedAnyClientState, connection: IdentifiedConnectionEnd, ) -> Result<(), Error> { - let handshake_enabled = self + let config_conn_enabled = self .config .read() .expect("poisoned lock") - .handshake_enabled(); + .mode + .connections + .enabled; let counterparty_chain = self .registry @@ -423,9 +430,9 @@ impl<'a, Chain: ChainHandle + 'static> SpawnContext<'a, Chain> { connection.connection_id, chain.id() ); - } else if !conn_state_dst.is_open() + } else if config_conn_enabled + && !conn_state_dst.is_open() && conn_state_dst.less_or_equal_progress(conn_state_src) - && handshake_enabled { // create worker for connection handshake that will advance the remote state let connection_object = Object::Connection(Connection { @@ -460,11 +467,8 @@ impl<'a, Chain: ChainHandle + 'static> SpawnContext<'a, Chain> { connection: &IdentifiedConnectionEnd, channel: IdentifiedChannelEnd, ) -> Result<(), Error> { - let handshake_enabled = self - .config - .read() - .expect("poisoned lock") - .handshake_enabled(); + let config = self.config.read().expect("poisoned lock"); + let mode = &config.mode; let counterparty_chain = self .registry @@ -488,63 +492,69 @@ impl<'a, Chain: ChainHandle + 'static> SpawnContext<'a, Chain> { chan_state_dst ); - if chan_state_src.is_open() + if (mode.clients.enabled || mode.packets.enabled) + && chan_state_src.is_open() && chan_state_dst.is_open() && self.relay_packets_on_channel(&chain, &channel) { - // spawn the client worker - let client_object = Object::Client(Client { - dst_client_id: client.client_id.clone(), - dst_chain_id: chain.id(), - src_chain_id: client.client_state.chain_id(), - }); - - self.workers - .spawn( - counterparty_chain.clone(), - chain.clone(), - &client_object, - &self.config.read().expect("poisoned lock"), - ) - .then(|| debug!("spawned Client worker: {}", client_object.short_name())); - - // Safe to unwrap because the inner channel end has state open - let counterparty_channel = counterparty_channel.unwrap(); - - let has_packets = || -> bool { - !unreceived_packets(&counterparty_chain, &chain, &counterparty_channel) - .unwrap_or_default() - .is_empty() - }; - - let has_acks = || -> bool { - !unreceived_acknowledgements(&counterparty_chain, &chain, &counterparty_channel) - .unwrap_or_default() - .is_empty() - }; - - // If there are any outstanding packets or acks to send, spawn the worker - if has_packets() || has_acks() { - // create the Packet object and spawn worker - let path_object = Object::Packet(Packet { - dst_chain_id: counterparty_chain.id(), - src_chain_id: chain.id(), - src_channel_id: channel.channel_id, - src_port_id: channel.port_id, + if mode.clients.enabled { + // Spawn the client worker + let client_object = Object::Client(Client { + dst_client_id: client.client_id.clone(), + dst_chain_id: chain.id(), + src_chain_id: client.client_state.chain_id(), }); self.workers .spawn( - chain.clone(), counterparty_chain.clone(), - &path_object, + chain.clone(), + &client_object, &self.config.read().expect("poisoned lock"), ) - .then(|| debug!("spawned Packet worker: {}", path_object.short_name())); + .then(|| debug!("spawned Client worker: {}", client_object.short_name())); + } + + if mode.packets.enabled { + // SAFETY: Safe to unwrap because the inner channel end has state open + let counterparty_channel = + counterparty_channel.expect("inner channel end is in state OPEN"); + + let has_packets = || -> bool { + !unreceived_packets(&counterparty_chain, &chain, &counterparty_channel) + .unwrap_or_default() + .is_empty() + }; + + let has_acks = || -> bool { + !unreceived_acknowledgements(&counterparty_chain, &chain, &counterparty_channel) + .unwrap_or_default() + .is_empty() + }; + + // If there are any outstanding packets or acks to send, spawn the worker + if has_packets() || has_acks() { + // Create the Packet object and spawn worker + let path_object = Object::Packet(Packet { + dst_chain_id: counterparty_chain.id(), + src_chain_id: chain.id(), + src_channel_id: channel.channel_id, + src_port_id: channel.port_id, + }); + + self.workers + .spawn( + chain.clone(), + counterparty_chain.clone(), + &path_object, + &self.config.read().expect("poisoned lock"), + ) + .then(|| debug!("spawned Packet worker: {}", path_object.short_name())); + } } - } else if !chan_state_dst.is_open() + } else if mode.channels.enabled + && !chan_state_dst.is_open() && chan_state_dst.less_or_equal_progress(chan_state_src) - && handshake_enabled { // create worker for channel handshake that will advance the remote state let channel_object = Object::Channel(Channel { diff --git a/relayer/src/worker.rs b/relayer/src/worker.rs index b9ed310ec9..fd3f80980c 100644 --- a/relayer/src/worker.rs +++ b/relayer/src/worker.rs @@ -91,9 +91,10 @@ impl Worker { - Self::Client(id, ClientWorker::new(client.clone(), chains, cmd_rx)) - } + Object::Client(client) => Self::Client( + id, + ClientWorker::new(client.clone(), chains, cmd_rx, config.mode.clients), + ), Object::Connection(connection) => Self::Connection( id, ConnectionWorker::new(connection.clone(), chains, cmd_rx), @@ -103,13 +104,7 @@ impl Worker Self::Packet( id, - PacketWorker::new( - path.clone(), - chains, - cmd_rx, - config.global.clear_packets_interval, - config.global.tx_confirmation, - ), + PacketWorker::new(path.clone(), chains, cmd_rx, config.mode.packets), ), }; diff --git a/relayer/src/worker/client.rs b/relayer/src/worker/client.rs index a98a1cd9ec..7020655428 100644 --- a/relayer/src/worker/client.rs +++ b/relayer/src/worker/client.rs @@ -8,6 +8,7 @@ use ibc::{core::ics02_client::events::UpdateClient, events::IbcEvent}; use crate::{ chain::handle::{ChainHandle, ChainHandlePair}, + config::Clients as ClientsConfig, foreign_client::{ForeignClient, ForeignClientErrorDetail, MisbehaviourResults}, object::Client, telemetry, @@ -20,6 +21,7 @@ pub struct ClientWorker { client: Client, chains: ChainHandlePair, cmd_rx: Receiver, + clients_cfg: ClientsConfig, } impl ClientWorker { @@ -27,11 +29,13 @@ impl ClientWorker { client: Client, chains: ChainHandlePair, cmd_rx: Receiver, + clients_cfg: ClientsConfig, ) -> Self { Self { client, chains, cmd_rx, + clients_cfg, } } @@ -44,12 +48,13 @@ impl ClientWorker { ); info!( - "[{}] running client worker & initial misbehaviour detection", - client + "[{}] running client worker with misbehaviour={} and refresh={}", + client, self.clients_cfg.misbehaviour, self.clients_cfg.refresh ); // initial check for evidence of misbehaviour for all updates - let skip_misbehaviour = self.detect_misbehaviour(&client, None); + let skip_misbehaviour = + !self.clients_cfg.misbehaviour || self.detect_misbehaviour(&client, None); // remember the time of the last refresh so we backoff let mut last_refresh = Instant::now() - Duration::from_secs(61); @@ -60,7 +65,7 @@ impl ClientWorker { // Clients typically need refresh every 2/3 of their // trusting period (which can e.g., two weeks). // Backoff refresh checking to attempt it every minute. - if last_refresh.elapsed() > Duration::from_secs(60) { + if self.clients_cfg.refresh && last_refresh.elapsed() > Duration::from_secs(60) { // Run client refresh, exit only if expired or frozen match client.refresh() { Ok(Some(_)) => { diff --git a/relayer/src/worker/packet.rs b/relayer/src/worker/packet.rs index 99045b72e9..1215d54c4f 100644 --- a/relayer/src/worker/packet.rs +++ b/relayer/src/worker/packet.rs @@ -1,10 +1,12 @@ use core::time::Duration; use crossbeam_channel::Receiver; +use ibc::Height; use tracing::{error, info, trace, warn}; use crate::{ chain::handle::{ChainHandle, ChainHandlePair}, + config::Packets as PacketsConfig, link::{Link, LinkParameters, RelaySummary}, object::Packet, telemetry, @@ -25,8 +27,8 @@ pub struct PacketWorker { path: Packet, chains: ChainHandlePair, cmd_rx: Receiver, - clear_packets_interval: u64, - with_tx_confirmation: bool, + packets_cfg: PacketsConfig, + first_run: bool, } impl PacketWorker { @@ -34,20 +36,33 @@ impl PacketWorker { path: Packet, chains: ChainHandlePair, cmd_rx: Receiver, - clear_packets_interval: u64, - with_tx_confirmation: bool, + packets_cfg: PacketsConfig, ) -> Self { Self { path, chains, cmd_rx, - clear_packets_interval, - with_tx_confirmation, + packets_cfg, + first_run: true, + } + } + + /// Whether or not to clear pending packets at this `step` for the given height. + /// Packets are cleared on the first iteration if `clear_on_start` is true. + /// Subsequently, packets are cleared only if `clear_interval` is not `0` and + /// if we have reached the interval. + fn clear_packets(&mut self, height: Height) -> bool { + if self.first_run { + self.first_run = false; + self.packets_cfg.clear_on_start + } else { + self.packets_cfg.clear_interval != 0 + && height.revision_height % self.packets_cfg.clear_interval == 0 } } /// Run the event loop for events associated with a [`Packet`]. - pub fn run(self) -> Result<(), RunError> { + pub fn run(mut self) -> Result<(), RunError> { let mut link = Link::new_from_opts( self.chains.a.clone(), self.chains.b.clone(), @@ -55,7 +70,7 @@ impl PacketWorker { src_port_id: self.path.src_port_id.clone(), src_channel_id: self.path.src_channel_id.clone(), }, - self.with_tx_confirmation, + self.packets_cfg.tx_confirmation, ) .map_err(RunError::link)?; @@ -110,7 +125,7 @@ impl PacketWorker { /// also refreshes and executes any scheduled operational /// data that is ready. fn step( - &self, + &mut self, cmd: Option, link: &mut Link, index: u64, @@ -125,13 +140,10 @@ impl PacketWorker { height, new_block: _, } => { - // Schedule the clearing of pending packets. This should happen - // once at start, and _forced_ at predefined block intervals. - let force_packet_clearing = self.clear_packets_interval != 0 - && height.revision_height % self.clear_packets_interval == 0; - + // Schedule the clearing of pending packets. This may happen once at start, + // and may be _forced_ at predefined block intervals. link.a_to_b - .schedule_packet_clearing(Some(height), force_packet_clearing) + .schedule_packet_clearing(Some(height), self.clear_packets(height)) } WorkerCmd::ClearPendingPackets => link.a_to_b.schedule_packet_clearing(None, true), diff --git a/relayer/tests/config/fixtures/relayer_conf_example.toml b/relayer/tests/config/fixtures/relayer_conf_example.toml index 44d001bcc5..13bcfc0162 100644 --- a/relayer/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/tests/config/fixtures/relayer_conf_example.toml @@ -1,7 +1,26 @@ [global] -strategy = 'packets' log_level = 'error' +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = false + +[mode.channels] +enabled = false + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true +filter = false +tx_confirmation = true + [[chains]] id = 'chain_A' rpc_addr = 'http://127.0.0.1:26657' diff --git a/scripts/gm/bin/lib-gm b/scripts/gm/bin/lib-gm index a3278b978f..fd1e21e7af 100644 --- a/scripts/gm/bin/lib-gm +++ b/scripts/gm/bin/lib-gm @@ -46,7 +46,6 @@ set_config_defaults() { GLOBAL_HDPATH="" GLOBAL_HERMES_BINARY="$(which hermes || echo "./hermes")" GLOBAL_HERMES_CONFIG="${HOME}/.hermes/config.toml" - GLOBAL_HERMES_STRATEGY="packets" GLOBAL_HERMES_LOG_LEVEL="info" GLOBAL_HERMES_TELEMETRY_ENABLED="true" GLOBAL_HERMES_TELEMETRY_HOST="127.0.0.1" @@ -83,8 +82,6 @@ parse_config_file() { # shellcheck disable=SC2155 export GLOBAL_HERMES_CONFIG="$(eval echo "$(stoml -sq "$CONFIG_FILE" global.hermes.config || echo "$GLOBAL_HERMES_CONFIG")")" # shellcheck disable=SC2155 - export GLOBAL_HERMES_STRATEGY="$(stoml -sq "$CONFIG_FILE" global.hermes.strategy || echo "$GLOBAL_HERMES_STRATEGY")" - # shellcheck disable=SC2155 export GLOBAL_HERMES_LOG_LEVEL="$(stoml -sq "$CONFIG_FILE" global.hermes.log_level || echo "$GLOBAL_HERMES_LOG_LEVEL")" # shellcheck disable=SC2155 export GLOBAL_HERMES_TELEMETRY_ENABLED="$(stoml -sq "$CONFIG_FILE" global.hermes.telemetry_enabled || echo "$GLOBAL_HERMES_TELEMETRY_ENABLED")" @@ -830,9 +827,28 @@ hermes_config() { fi cat < "$GLOBAL_HERMES_CONFIG" [global] -strategy = '${GLOBAL_HERMES_STRATEGY}' log_level = '${GLOBAL_HERMES_LOG_LEVEL}' +[mode] + +[mode.clients] +enabled = true +refresh = true +misbehaviour = true + +[mode.connections] +enabled = true + +[mode.channels] +enabled = true + +[mode.packets] +enabled = true +clear_interval = 100 +clear_on_start = true +filter = false +tx_confirmation = true + [telemetry] enabled = ${GLOBAL_HERMES_TELEMETRY_ENABLED} host = '${GLOBAL_HERMES_TELEMETRY_HOST}' diff --git a/scripts/gm/gm.toml b/scripts/gm/gm.toml index 56d4361eec..0d7c393008 100644 --- a/scripts/gm/gm.toml +++ b/scripts/gm/gm.toml @@ -59,9 +59,6 @@ binary="./hermes" # Hermes configuration file path. config="$HOME/.hermes/config.toml" -# Hermes configuration strategy paremeter. -strategy="packets" - # Hermes configuration log_level parameter. log_level="info"