diff --git a/bridges/bin/millau/node/src/chain_spec.rs b/bridges/bin/millau/node/src/chain_spec.rs index 583510fc34..66ce0e2311 100644 --- a/bridges/bin/millau/node/src/chain_spec.rs +++ b/bridges/bin/millau/node/src/chain_spec.rs @@ -16,8 +16,8 @@ use bp_millau::derive_account_from_rialto_id; use millau_runtime::{ - AccountId, AuraConfig, BalancesConfig, BridgeWestendGrandpaConfig, GenesisConfig, GrandpaConfig, SessionConfig, - SessionKeys, Signature, SudoConfig, SystemConfig, WASM_BINARY, + AccountId, AuraConfig, BalancesConfig, BridgeRialtoMessagesConfig, BridgeWestendGrandpaConfig, GenesisConfig, + GrandpaConfig, SessionConfig, SessionKeys, Signature, SudoConfig, SystemConfig, WASM_BINARY, }; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{sr25519, Pair, Public}; @@ -134,6 +134,7 @@ impl Alternative { get_account_id_from_seed::("Ferdie//stash"), get_account_id_from_seed::("George//stash"), get_account_id_from_seed::("Harry//stash"), + get_account_id_from_seed::("RialtoMessagesOwner"), pallet_bridge_messages::Pallet::< millau_runtime::Runtime, millau_runtime::WithRialtoMessagesInstance, @@ -208,6 +209,10 @@ fn testnet_genesis( owner: Some(get_account_id_from_seed::("George")), ..Default::default() }, + bridge_rialto_messages: BridgeRialtoMessagesConfig { + owner: Some(get_account_id_from_seed::("RialtoMessagesOwner")), + ..Default::default() + }, } } diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs index ed7580906f..9bbdf3a937 100644 --- a/bridges/bin/millau/runtime/src/lib.rs +++ b/bridges/bin/millau/runtime/src/lib.rs @@ -407,7 +407,7 @@ construct_runtime!( NodeBlock = opaque::Block, UncheckedExtrinsic = UncheckedExtrinsic { - BridgeRialtoMessages: pallet_bridge_messages::{Pallet, Call, Storage, Event}, + BridgeRialtoMessages: pallet_bridge_messages::{Pallet, Call, Storage, Event, Config}, BridgeDispatch: pallet_bridge_dispatch::{Pallet, Event}, BridgeRialtoGrandpa: pallet_bridge_grandpa::{Pallet, Call, Storage}, BridgeWestendGrandpa: pallet_bridge_grandpa::::{Pallet, Call, Config, Storage}, diff --git a/bridges/bin/rialto/node/src/chain_spec.rs b/bridges/bin/rialto/node/src/chain_spec.rs index 60b1af24e3..977ee17946 100644 --- a/bridges/bin/rialto/node/src/chain_spec.rs +++ b/bridges/bin/rialto/node/src/chain_spec.rs @@ -16,8 +16,8 @@ use bp_rialto::derive_account_from_millau_id; use rialto_runtime::{ - AccountId, BabeConfig, BalancesConfig, BridgeKovanConfig, BridgeRialtoPoaConfig, GenesisConfig, GrandpaConfig, - SessionConfig, SessionKeys, Signature, SudoConfig, SystemConfig, WASM_BINARY, + AccountId, BabeConfig, BalancesConfig, BridgeKovanConfig, BridgeMillauMessagesConfig, BridgeRialtoPoaConfig, + GenesisConfig, GrandpaConfig, SessionConfig, SessionKeys, Signature, SudoConfig, SystemConfig, WASM_BINARY, }; use serde_json::json; use sp_consensus_babe::AuthorityId as BabeId; @@ -135,6 +135,7 @@ impl Alternative { get_account_id_from_seed::("Ferdie//stash"), get_account_id_from_seed::("George//stash"), get_account_id_from_seed::("Harry//stash"), + get_account_id_from_seed::("MillauMessagesOwner"), pallet_bridge_messages::Pallet::< rialto_runtime::Runtime, rialto_runtime::WithMillauMessagesInstance, @@ -205,6 +206,10 @@ fn testnet_genesis( .map(|x| (x.0.clone(), x.0.clone(), session_keys(x.1.clone(), x.2.clone()))) .collect::>(), }, + bridge_millau_messages: BridgeMillauMessagesConfig { + owner: Some(get_account_id_from_seed::("MillauMessagesOwner")), + ..Default::default() + }, } } diff --git a/bridges/bin/rialto/runtime/src/lib.rs b/bridges/bin/rialto/runtime/src/lib.rs index 75ba22389a..2899d18d14 100644 --- a/bridges/bin/rialto/runtime/src/lib.rs +++ b/bridges/bin/rialto/runtime/src/lib.rs @@ -559,7 +559,7 @@ construct_runtime!( // Millau bridge modules. BridgeMillauGrandpa: pallet_bridge_grandpa::{Pallet, Call, Storage}, BridgeDispatch: pallet_bridge_dispatch::{Pallet, Event}, - BridgeMillauMessages: pallet_bridge_messages::{Pallet, Call, Storage, Event}, + BridgeMillauMessages: pallet_bridge_messages::{Pallet, Call, Storage, Event, Config}, } ); diff --git a/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs b/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs index 6b861c28d4..b63d57aeb7 100644 --- a/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs +++ b/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs @@ -245,3 +245,33 @@ pub(crate) fn add_standalone_metrics( )), ) } + +/// Update Rialto -> Millau conversion rate, stored in Millau runtime storage. +pub(crate) async fn update_rialto_to_millau_conversion_rate( + client: Client, + signer: ::AccountKeyPair, + updated_rate: f64, +) -> anyhow::Result<()> { + let genesis_hash = *client.genesis_hash(); + let signer_id = (*signer.public().as_array_ref()).into(); + client + .submit_signed_extrinsic(signer_id, move |transaction_nonce| { + Bytes( + Millau::sign_transaction( + genesis_hash, + &signer, + transaction_nonce, + millau_runtime::MessagesCall::update_pallet_parameter( + millau_runtime::rialto_messages::MillauToRialtoMessagesParameter::RialtoToMillauConversionRate( + sp_runtime::FixedU128::from_float(updated_rate), + ), + ) + .into(), + ) + .encode(), + ) + }) + .await + .map(drop) + .map_err(|err| anyhow::format_err!("{:?}", err)) +} diff --git a/bridges/relays/bin-substrate/src/chains/mod.rs b/bridges/relays/bin-substrate/src/chains/mod.rs index d2b72d4448..8a9cb780b4 100644 --- a/bridges/relays/bin-substrate/src/chains/mod.rs +++ b/bridges/relays/bin-substrate/src/chains/mod.rs @@ -37,9 +37,9 @@ mod wococo; // Rialto as BTC and Millau as wBTC (only in relayer). /// The identifier of token, which value is associated with Rialto token value by relayer. -pub(crate) const RIALTO_ASSOCIATED_TOKEN_ID: &str = "bitcoin"; +pub(crate) const RIALTO_ASSOCIATED_TOKEN_ID: &str = "polkadot"; /// The identifier of token, which value is associated with Millau token value by relayer. -pub(crate) const MILLAU_ASSOCIATED_TOKEN_ID: &str = "wrapped-bitcoin"; +pub(crate) const MILLAU_ASSOCIATED_TOKEN_ID: &str = "kusama"; use relay_utils::metrics::MetricsParams; diff --git a/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs b/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs index f2ee1f82f5..1f861d1bb5 100644 --- a/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs +++ b/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs @@ -244,3 +244,33 @@ pub(crate) fn add_standalone_metrics( )), ) } + +/// Update Millau -> Rialto conversion rate, stored in Rialto runtime storage. +pub(crate) async fn update_millau_to_rialto_conversion_rate( + client: Client, + signer: ::AccountKeyPair, + updated_rate: f64, +) -> anyhow::Result<()> { + let genesis_hash = *client.genesis_hash(); + let signer_id = (*signer.public().as_array_ref()).into(); + client + .submit_signed_extrinsic(signer_id, move |transaction_nonce| { + Bytes( + Rialto::sign_transaction( + genesis_hash, + &signer, + transaction_nonce, + rialto_runtime::MessagesCall::update_pallet_parameter( + rialto_runtime::millau_messages::RialtoToMillauMessagesParameter::MillauToRialtoConversionRate( + sp_runtime::FixedU128::from_float(updated_rate), + ), + ) + .into(), + ) + .encode(), + ) + }) + .await + .map(drop) + .map_err(|err| anyhow::format_err!("{:?}", err)) +} diff --git a/bridges/relays/bin-substrate/src/cli/mod.rs b/bridges/relays/bin-substrate/src/cli/mod.rs index d108c71f7b..832b530877 100644 --- a/bridges/relays/bin-substrate/src/cli/mod.rs +++ b/bridges/relays/bin-substrate/src/cli/mod.rs @@ -391,6 +391,17 @@ macro_rules! declare_chain_options { pub [<$chain_prefix _signer_password_file>]: Option, } + #[doc = "Parameters required to sign transaction on behalf of owner of the messages pallet at " $chain "."] + #[derive(StructOpt, Debug, PartialEq, Eq)] + pub struct [<$chain MessagesPalletOwnerSigningParams>] { + #[doc = "The SURI of secret key to use when transactions are submitted to the " $chain " node."] + #[structopt(long)] + pub [<$chain_prefix _messages_pallet_owner>]: Option, + #[doc = "The password for the SURI of secret key to use when transactions are submitted to the " $chain " node."] + #[structopt(long)] + pub [<$chain_prefix _messages_pallet_owner_password>]: Option, + } + impl [<$chain SigningParams>] { /// Parse signing params into chain-specific KeyPair. pub fn to_keypair(&self) -> anyhow::Result { @@ -433,6 +444,23 @@ macro_rules! declare_chain_options { } } + #[allow(dead_code)] + impl [<$chain MessagesPalletOwnerSigningParams>] { + /// Parse signing params into chain-specific KeyPair. + pub fn to_keypair(&self) -> anyhow::Result> { + use sp_core::crypto::Pair; + + let [<$chain_prefix _messages_pallet_owner>] = match self.[<$chain_prefix _messages_pallet_owner>] { + Some(ref messages_pallet_owner) => messages_pallet_owner, + None => return Ok(None), + }; + Chain::KeyPair::from_string( + [<$chain_prefix _messages_pallet_owner>], + self.[<$chain_prefix _messages_pallet_owner_password>].as_deref() + ).map_err(|e| anyhow::format_err!("{:?}", e)).map(Some) + } + } + impl [<$chain ConnectionParams>] { /// Convert connection params into Substrate client. pub async fn to_client( diff --git a/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs b/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs index 4747939085..a2f0d236a9 100644 --- a/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs +++ b/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs @@ -26,6 +26,7 @@ use futures::{FutureExt, TryFutureExt}; use structopt::StructOpt; use strum::VariantNames; +use relay_substrate_client::{Chain, Client, TransactionSignScheme}; use relay_utils::metrics::MetricsParams; use substrate_relay_helper::messages_lane::{MessagesRelayParams, SubstrateMessageLane}; use substrate_relay_helper::on_demand_headers::OnDemandHeadersRelay; @@ -33,6 +34,14 @@ use substrate_relay_helper::on_demand_headers::OnDemandHeadersRelay; use crate::cli::{relay_messages::RelayerMode, CliChain, HexLaneId, PrometheusParams}; use crate::declare_chain_options; +/// Maximal allowed conversion rate error ratio (abs(real - stored) / stored) that we allow. +/// +/// If it is zero, then transaction will be submitted every time we see difference between +/// stored and real conversion rates. If it is large enough (e.g. > than 10 percents, which is 0.1), +/// then rational relayers may stop relaying messages because they were submitted using +/// lesser conversion rate. +const CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO: f64 = 0.05; + /// Start headers+messages relayer process. #[derive(StructOpt)] pub enum RelayHeadersAndMessages { @@ -68,9 +77,13 @@ macro_rules! declare_bridge_options { #[structopt(flatten)] left_sign: [<$chain1 SigningParams>], #[structopt(flatten)] + left_messages_pallet_owner: [<$chain1 MessagesPalletOwnerSigningParams>], + #[structopt(flatten)] right: [<$chain2 ConnectionParams>], #[structopt(flatten)] right_sign: [<$chain2 SigningParams>], + #[structopt(flatten)] + right_messages_pallet_owner: [<$chain2 MessagesPalletOwnerSigningParams>], } #[allow(unreachable_patterns)] @@ -106,9 +119,11 @@ macro_rules! select_bridge { use crate::chains::millau_messages_to_rialto::{ add_standalone_metrics as add_left_to_right_standalone_metrics, run as left_to_right_messages, + update_rialto_to_millau_conversion_rate as update_right_to_left_conversion_rate, }; use crate::chains::rialto_messages_to_millau::{ add_standalone_metrics as add_right_to_left_standalone_metrics, run as right_to_left_messages, + update_millau_to_rialto_conversion_rate as update_left_to_right_conversion_rate, }; $generic @@ -135,6 +150,22 @@ macro_rules! select_bridge { add_standalone_metrics as add_right_to_left_standalone_metrics, run as right_to_left_messages, }; + async fn update_right_to_left_conversion_rate( + _client: Client, + _signer: ::AccountKeyPair, + _updated_rate: f64, + ) -> anyhow::Result<()> { + Err(anyhow::format_err!("Conversion rate is not supported by this bridge")) + } + + async fn update_left_to_right_conversion_rate( + _client: Client, + _signer: ::AccountKeyPair, + _updated_rate: f64, + ) -> anyhow::Result<()> { + Err(anyhow::format_err!("Conversion rate is not supported by this bridge")) + } + $generic } } @@ -158,16 +189,86 @@ impl RelayHeadersAndMessages { let left_client = params.left.to_client::().await?; let left_sign = params.left_sign.to_keypair::()?; + let left_messages_pallet_owner = params.left_messages_pallet_owner.to_keypair::()?; let right_client = params.right.to_client::().await?; let right_sign = params.right_sign.to_keypair::()?; + let right_messages_pallet_owner = params.right_messages_pallet_owner.to_keypair::()?; let lanes = params.shared.lane; let relayer_mode = params.shared.relayer_mode.into(); + const METRIC_IS_SOME_PROOF: &str = "it is `None` when metric has been already registered; \ + this is the command entrypoint, so nothing has been registered yet; \ + qed"; + let metrics_params: MetricsParams = params.shared.prometheus_params.into(); let metrics_params = relay_utils::relay_metrics(None, metrics_params).into_params(); - let (metrics_params, _) = add_left_to_right_standalone_metrics(None, metrics_params, left_client.clone())?; - let (metrics_params, _) = add_right_to_left_standalone_metrics(None, metrics_params, right_client.clone())?; + let (metrics_params, left_to_right_metrics) = + add_left_to_right_standalone_metrics(None, metrics_params, left_client.clone())?; + let (metrics_params, right_to_left_metrics) = + add_right_to_left_standalone_metrics(None, metrics_params, right_client.clone())?; + if let Some(left_messages_pallet_owner) = left_messages_pallet_owner { + let left_client = left_client.clone(); + substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop( + left_to_right_metrics + .target_to_source_conversion_rate + .expect(METRIC_IS_SOME_PROOF), + left_to_right_metrics + .target_to_base_conversion_rate + .clone() + .expect(METRIC_IS_SOME_PROOF), + left_to_right_metrics + .source_to_base_conversion_rate + .clone() + .expect(METRIC_IS_SOME_PROOF), + CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO, + move |new_rate| { + log::info!( + target: "bridge", + "Going to update {} -> {} (on {}) conversion rate to {}.", + Right::NAME, + Left::NAME, + Left::NAME, + new_rate, + ); + update_right_to_left_conversion_rate( + left_client.clone(), + left_messages_pallet_owner.clone(), + new_rate, + ) + }, + ); + } + if let Some(right_messages_pallet_owner) = right_messages_pallet_owner { + let right_client = right_client.clone(); + substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop( + right_to_left_metrics + .target_to_source_conversion_rate + .expect(METRIC_IS_SOME_PROOF), + left_to_right_metrics + .source_to_base_conversion_rate + .expect(METRIC_IS_SOME_PROOF), + left_to_right_metrics + .target_to_base_conversion_rate + .expect(METRIC_IS_SOME_PROOF), + CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO, + move |new_rate| { + log::info!( + target: "bridge", + "Going to update {} -> {} (on {}) conversion rate to {}.", + Left::NAME, + Right::NAME, + Right::NAME, + new_rate, + ); + update_left_to_right_conversion_rate( + right_client.clone(), + right_messages_pallet_owner.clone(), + new_rate, + ) + }, + ); + } let left_to_right_on_demand_headers = OnDemandHeadersRelay::new( left_client.clone(), diff --git a/bridges/relays/client-substrate/src/metrics/float_storage_value.rs b/bridges/relays/client-substrate/src/metrics/float_storage_value.rs index f3ba8988ee..e598885227 100644 --- a/bridges/relays/client-substrate/src/metrics/float_storage_value.rs +++ b/bridges/relays/client-substrate/src/metrics/float_storage_value.rs @@ -17,9 +17,12 @@ use crate::chain::Chain; use crate::client::Client; +use async_std::sync::{Arc, RwLock}; use async_trait::async_trait; use codec::Decode; -use relay_utils::metrics::{metric_name, register, Gauge, PrometheusError, Registry, StandaloneMetrics, F64}; +use relay_utils::metrics::{ + metric_name, register, F64SharedRef, Gauge, PrometheusError, Registry, StandaloneMetrics, F64, +}; use sp_core::storage::StorageKey; use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber}; use std::time::Duration; @@ -34,6 +37,7 @@ pub struct FloatStorageValueMetric { storage_key: StorageKey, maybe_default_value: Option, metric: Gauge, + shared_value_ref: F64SharedRef, } impl FloatStorageValueMetric { @@ -47,13 +51,20 @@ impl FloatStorageValueMetric { name: String, help: String, ) -> Result { + let shared_value_ref = Arc::new(RwLock::new(None)); Ok(FloatStorageValueMetric { client, storage_key, maybe_default_value, metric: register(Gauge::new(metric_name(prefix, &name), help)?, registry)?, + shared_value_ref, }) } + + /// Get shared reference to metric value. + pub fn shared_value_ref(&self) -> F64SharedRef { + self.shared_value_ref.clone() + } } #[async_trait] @@ -66,17 +77,17 @@ where } async fn update(&self) { - relay_utils::metrics::set_gauge_value( - &self.metric, - self.client - .storage_value::(self.storage_key.clone()) - .await - .map(|maybe_storage_value| { - maybe_storage_value.or(self.maybe_default_value).map(|storage_value| { - storage_value.into_inner().unique_saturated_into() as f64 - / T::DIV.unique_saturated_into() as f64 - }) - }), - ); + let value = self + .client + .storage_value::(self.storage_key.clone()) + .await + .map(|maybe_storage_value| { + maybe_storage_value.or(self.maybe_default_value).map(|storage_value| { + storage_value.into_inner().unique_saturated_into() as f64 / T::DIV.unique_saturated_into() as f64 + }) + }) + .map_err(drop); + relay_utils::metrics::set_gauge_value(&self.metric, value); + *self.shared_value_ref.write().await = value.ok().and_then(|x| x); } } diff --git a/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs b/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs new file mode 100644 index 0000000000..ee02672b48 --- /dev/null +++ b/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs @@ -0,0 +1,210 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Tools for updating conversion rate that is stored in the runtime storage. + +use relay_utils::metrics::F64SharedRef; +use std::{future::Future, time::Duration}; + +/// Duration between updater wakeups. +const SLEEP_DURATION: Duration = Duration::from_secs(60); + +/// Update-conversion-rate transaction status. +#[derive(Debug, Clone, Copy, PartialEq)] +enum TransactionStatus { + /// We have not submitted any transaction recently. + Idle, + /// We have recently submitted transaction that should update conversion rate. + Submitted(f64), +} + +/// Run infinite conversion rate updater loop. +/// +/// The loop is maintaining the Left -> Right conversion rate, used as `RightTokens = LeftTokens * Rate`. +pub fn run_conversion_rate_update_loop< + SubmitConversionRateFuture: Future> + Send + 'static, +>( + left_to_right_stored_conversion_rate: F64SharedRef, + left_to_base_conversion_rate: F64SharedRef, + right_to_base_conversion_rate: F64SharedRef, + max_difference_ratio: f64, + submit_conversion_rate: impl Fn(f64) -> SubmitConversionRateFuture + Send + 'static, +) { + async_std::task::spawn(async move { + let mut transaction_status = TransactionStatus::Idle; + loop { + async_std::task::sleep(SLEEP_DURATION).await; + let maybe_new_conversion_rate = maybe_select_new_conversion_rate( + &mut transaction_status, + &left_to_right_stored_conversion_rate, + &left_to_base_conversion_rate, + &right_to_base_conversion_rate, + max_difference_ratio, + ) + .await; + if let Some((prev_conversion_rate, new_conversion_rate)) = maybe_new_conversion_rate { + let submit_conversion_rate_future = submit_conversion_rate(new_conversion_rate); + match submit_conversion_rate_future.await { + Ok(()) => { + transaction_status = TransactionStatus::Submitted(prev_conversion_rate); + } + Err(error) => { + log::trace!(target: "bridge", "Failed to submit conversion rate update transaction: {:?}", error); + } + } + } + } + }); +} + +/// Select new conversion rate to submit to the node. +async fn maybe_select_new_conversion_rate( + transaction_status: &mut TransactionStatus, + left_to_right_stored_conversion_rate: &F64SharedRef, + left_to_base_conversion_rate: &F64SharedRef, + right_to_base_conversion_rate: &F64SharedRef, + max_difference_ratio: f64, +) -> Option<(f64, f64)> { + let left_to_right_stored_conversion_rate = (*left_to_right_stored_conversion_rate.read().await)?; + match *transaction_status { + TransactionStatus::Idle => (), + TransactionStatus::Submitted(previous_left_to_right_stored_conversion_rate) => { + // we can't compare float values from different sources directly, so we only care whether the + // stored rate has been changed or not. If it has been changed, then we assume that our proposal + // has been accepted. + // + // float comparison is ok here, because we compare same-origin (stored in runtime storage) values + // and if they are different, it means that the value has actually been updated + #[allow(clippy::float_cmp)] + if previous_left_to_right_stored_conversion_rate == left_to_right_stored_conversion_rate { + // the rate has not been changed => we won't submit any transactions until it is accepted, + // or the rate is changed by someone else + return None; + } + + *transaction_status = TransactionStatus::Idle; + } + } + + let left_to_base_conversion_rate = (*left_to_base_conversion_rate.read().await)?; + let right_to_base_conversion_rate = (*right_to_base_conversion_rate.read().await)?; + let actual_left_to_right_conversion_rate = right_to_base_conversion_rate / left_to_base_conversion_rate; + + let rate_difference = (actual_left_to_right_conversion_rate - left_to_right_stored_conversion_rate).abs(); + let rate_difference_ratio = rate_difference / left_to_right_stored_conversion_rate; + if rate_difference_ratio < max_difference_ratio { + return None; + } + + Some(( + left_to_right_stored_conversion_rate, + actual_left_to_right_conversion_rate, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use async_std::sync::{Arc, RwLock}; + + fn test_maybe_select_new_conversion_rate( + mut transaction_status: TransactionStatus, + stored_conversion_rate: Option, + left_to_base_conversion_rate: Option, + right_to_base_conversion_rate: Option, + max_difference_ratio: f64, + ) -> (Option<(f64, f64)>, TransactionStatus) { + let stored_conversion_rate = Arc::new(RwLock::new(stored_conversion_rate)); + let left_to_base_conversion_rate = Arc::new(RwLock::new(left_to_base_conversion_rate)); + let right_to_base_conversion_rate = Arc::new(RwLock::new(right_to_base_conversion_rate)); + let result = async_std::task::block_on(maybe_select_new_conversion_rate( + &mut transaction_status, + &stored_conversion_rate, + &left_to_base_conversion_rate, + &right_to_base_conversion_rate, + max_difference_ratio, + )); + (result, transaction_status) + } + + #[test] + fn rate_is_not_updated_when_transaction_is_submitted() { + assert_eq!( + test_maybe_select_new_conversion_rate( + TransactionStatus::Submitted(10.0), + Some(10.0), + Some(1.0), + Some(1.0), + 0.0 + ), + (None, TransactionStatus::Submitted(10.0)), + ); + } + + #[test] + fn transaction_state_is_changed_to_idle_when_stored_rate_shanges() { + assert_eq!( + test_maybe_select_new_conversion_rate( + TransactionStatus::Submitted(1.0), + Some(10.0), + Some(1.0), + Some(1.0), + 100.0 + ), + (None, TransactionStatus::Idle), + ); + } + + #[test] + fn transaction_is_not_submitted_when_left_to_base_rate_is_unknown() { + assert_eq!( + test_maybe_select_new_conversion_rate(TransactionStatus::Idle, Some(10.0), None, Some(1.0), 0.0), + (None, TransactionStatus::Idle), + ); + } + + #[test] + fn transaction_is_not_submitted_when_right_to_base_rate_is_unknown() { + assert_eq!( + test_maybe_select_new_conversion_rate(TransactionStatus::Idle, Some(10.0), Some(1.0), None, 0.0), + (None, TransactionStatus::Idle), + ); + } + + #[test] + fn transaction_is_not_submitted_when_stored_rate_is_unknown() { + assert_eq!( + test_maybe_select_new_conversion_rate(TransactionStatus::Idle, None, Some(1.0), Some(1.0), 0.0), + (None, TransactionStatus::Idle), + ); + } + + #[test] + fn transaction_is_not_submitted_when_difference_is_below_threshold() { + assert_eq!( + test_maybe_select_new_conversion_rate(TransactionStatus::Idle, Some(1.0), Some(1.0), Some(1.01), 0.02), + (None, TransactionStatus::Idle), + ); + } + + #[test] + fn transaction_is_submitted_when_difference_is_above_threshold() { + assert_eq!( + test_maybe_select_new_conversion_rate(TransactionStatus::Idle, Some(1.0), Some(1.0), Some(1.03), 0.02), + (Some((1.0, 1.03)), TransactionStatus::Idle), + ); + } +} diff --git a/bridges/relays/lib-substrate-relay/src/lib.rs b/bridges/relays/lib-substrate-relay/src/lib.rs index 32eaa2276e..380bcfef2d 100644 --- a/bridges/relays/lib-substrate-relay/src/lib.rs +++ b/bridges/relays/lib-substrate-relay/src/lib.rs @@ -18,6 +18,7 @@ #![warn(missing_docs)] +pub mod conversion_rate_update; pub mod finality_pipeline; pub mod finality_target; pub mod headers_initialize; diff --git a/bridges/relays/lib-substrate-relay/src/messages_lane.rs b/bridges/relays/lib-substrate-relay/src/messages_lane.rs index c859e0f9e2..0b648e8cc8 100644 --- a/bridges/relays/lib-substrate-relay/src/messages_lane.rs +++ b/bridges/relays/lib-substrate-relay/src/messages_lane.rs @@ -20,6 +20,7 @@ use crate::messages_source::SubstrateMessagesProof; use crate::messages_target::SubstrateMessagesReceivingProof; use crate::on_demand_headers::OnDemandHeadersRelay; +use async_trait::async_trait; use bp_messages::{LaneId, MessageNonce}; use frame_support::weights::Weight; use messages_relay::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf}; @@ -58,6 +59,7 @@ pub struct MessagesRelayParams { } /// Message sync pipeline for Substrate <-> Substrate relays. +#[async_trait] pub trait SubstrateMessageLane: 'static + Clone + Send + Sync { /// Underlying generic message lane. type MessageLane: MessageLane; @@ -211,6 +213,8 @@ pub struct StandaloneMessagesMetrics { pub target_to_base_conversion_rate: Option, /// Shared reference to the actual source -> chain token conversion rate. pub source_to_base_conversion_rate: Option, + /// Shared reference to the stored (in the source chain runtime storage) target -> source chain conversion rate. + pub target_to_source_conversion_rate: Option, } impl StandaloneMessagesMetrics { @@ -218,7 +222,7 @@ impl StandaloneMessagesMetrics { pub async fn target_to_source_conversion_rate(&self) -> Option { let target_to_base_conversion_rate = (*self.target_to_base_conversion_rate.as_ref()?.read().await)?; let source_to_base_conversion_rate = (*self.source_to_base_conversion_rate.as_ref()?.read().await)?; - Some(target_to_base_conversion_rate / source_to_base_conversion_rate) + Some(source_to_base_conversion_rate / target_to_base_conversion_rate) } } @@ -231,6 +235,7 @@ pub fn add_standalone_metrics( target_chain_token_id: Option<&str>, target_to_source_conversion_rate_params: Option<(StorageKey, FixedU128)>, ) -> anyhow::Result<(MetricsParams, StandaloneMessagesMetrics)> { + let mut target_to_source_conversion_rate = None; let mut source_to_base_conversion_rate = None; let mut target_to_base_conversion_rate = None; let mut metrics_params = @@ -266,6 +271,7 @@ pub fn add_standalone_metrics( P::SourceChain::NAME ), )?; + target_to_source_conversion_rate = Some(metric.shared_value_ref()); Ok(metric) })?; } @@ -288,6 +294,7 @@ pub fn add_standalone_metrics( StandaloneMessagesMetrics { target_to_base_conversion_rate, source_to_base_conversion_rate, + target_to_source_conversion_rate, }, )) } @@ -295,6 +302,7 @@ pub fn add_standalone_metrics( #[cfg(test)] mod tests { use super::*; + use async_std::sync::{Arc, RwLock}; type RialtoToMillauMessagesWeights = pallet_bridge_messages::weights::RialtoWeight; @@ -314,4 +322,15 @@ mod tests { (782, 216_583_333_334), ); } + + #[async_std::test] + async fn target_to_source_conversion_rate_works() { + let metrics = StandaloneMessagesMetrics { + target_to_base_conversion_rate: Some(Arc::new(RwLock::new(Some(183.15)))), + source_to_base_conversion_rate: Some(Arc::new(RwLock::new(Some(12.32)))), + target_to_source_conversion_rate: None, // we don't care + }; + + assert_eq!(metrics.target_to_source_conversion_rate().await, Some(12.32 / 183.15),); + } }