diff --git a/bridges/bin/millau/runtime/Cargo.toml b/bridges/bin/millau/runtime/Cargo.toml index 598b100e1ab8..97c6878eb843 100644 --- a/bridges/bin/millau/runtime/Cargo.toml +++ b/bridges/bin/millau/runtime/Cargo.toml @@ -26,6 +26,7 @@ bp-westend = { path = "../../../primitives/chain-westend", default-features = fa bridge-runtime-common = { path = "../../runtime-common", default-features = false } pallet-bridge-grandpa = { path = "../../../modules/grandpa", default-features = false } pallet-bridge-messages = { path = "../../../modules/messages", default-features = false } +pallet-bridge-parachains = { path = "../../../modules/parachains", default-features = false } pallet-shift-session-manager = { path = "../../../modules/shift-session-manager", default-features = false } # Substrate Dependencies @@ -102,6 +103,7 @@ std = [ "pallet-beefy-mmr/std", "pallet-bridge-grandpa/std", "pallet-bridge-messages/std", + "pallet-bridge-parachains/std", "pallet-grandpa/std", "pallet-mmr/std", "pallet-randomness-collective-flip/std", diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs index 9d59d85d88e2..6f38f7a54ed5 100644 --- a/bridges/bin/millau/runtime/src/lib.rs +++ b/bridges/bin/millau/runtime/src/lib.rs @@ -73,6 +73,7 @@ pub use frame_system::Call as SystemCall; pub use pallet_balances::Call as BalancesCall; pub use pallet_bridge_grandpa::Call as BridgeGrandpaCall; pub use pallet_bridge_messages::Call as MessagesCall; +pub use pallet_bridge_parachains::Call as BridgeParachainsCall; pub use pallet_sudo::Call as SudoCall; pub use pallet_timestamp::Call as TimestampCall; @@ -461,6 +462,19 @@ impl pallet_bridge_messages::Config for Runtime { type BridgedChainId = RialtoChainId; } +parameter_types! { + pub const RialtoParasPalletName: &'static str = bp_rialto::PARAS_PALLET_NAME; +} + +/// Instance of the with-Rialto parachains token swap pallet. +pub type WitRialtoParachainsInstance = (); + +impl pallet_bridge_parachains::Config for Runtime { + type BridgesGrandpaPalletInstance = RialtoGrandpaInstance; + type ParasPalletName = RialtoParasPalletName; + type HeadsToKeep = HeadersToKeep; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -495,6 +509,9 @@ construct_runtime!( // Westend bridge modules. BridgeWestendGrandpa: pallet_bridge_grandpa::::{Pallet, Call, Config, Storage}, + // Rialto parachains bridge modules. + BridgeRialtoParachains: pallet_bridge_parachains::{Pallet, Call, Storage}, + // Pallet for sending XCM. XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin, Config} = 99, } diff --git a/bridges/modules/parachains/Cargo.toml b/bridges/modules/parachains/Cargo.toml index 6ed9552e56b6..e6942bf7ce34 100644 --- a/bridges/modules/parachains/Cargo.toml +++ b/bridges/modules/parachains/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0.101", optional = true } # Bridge Dependencies +bp-parachains = { path = "../../primitives/parachains", default-features = false } bp-polkadot-core = { path = "../../primitives/polkadot-core", default-features = false } bp-runtime = { path = "../../primitives/runtime", default-features = false } pallet-bridge-grandpa = { path = "../grandpa", default-features = false } @@ -34,6 +35,7 @@ sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" } [features] default = ["std"] std = [ + "bp-parachains/std", "bp-polkadot-core/std", "bp-runtime/std", "codec/std", diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs index 3f5d214f61d5..9bed2b7a18a9 100644 --- a/bridges/modules/parachains/src/lib.rs +++ b/bridges/modules/parachains/src/lib.rs @@ -23,7 +23,8 @@ #![cfg_attr(not(feature = "std"), no_std)] -use bp_polkadot_core::parachains::{ParaHash, ParaHead, ParaId, ParachainHeadsProof}; +use bp_parachains::parachain_head_storage_key_at_source; +use bp_polkadot_core::parachains::{ParaHash, ParaHead, ParaHeadsProof, ParaId}; use codec::{Decode, Encode}; use frame_support::RuntimeDebug; use scale_info::TypeInfo; @@ -73,12 +74,16 @@ pub mod pallet { pub trait Config: pallet_bridge_grandpa::Config { - /// Instance of bridges GRANDPA pallet that this pallet is linked to. + /// Instance of bridges GRANDPA pallet (within this runtime) that this pallet is linked to. /// /// The GRANDPA pallet instance must be configured to import headers of relay chain that /// we're interested in. type BridgesGrandpaPalletInstance: 'static; + /// Name of the `paras` pallet in the `construct_runtime!()` call at the bridged chain. + #[pallet::constant] + type ParasPalletName: Get<&'static str>; + /// Maximal number of single parachain heads to keep in the storage. /// /// The setting is there to prevent growing the on-chain state indefinitely. Note @@ -129,7 +134,7 @@ pub mod pallet { _origin: OriginFor, relay_block_hash: RelayBlockHash, parachains: Vec, - parachain_heads_proof: ParachainHeadsProof, + parachain_heads_proof: ParaHeadsProof, ) -> DispatchResult { // we'll need relay chain header to verify that parachains heads are always increasing. let relay_block = pallet_bridge_grandpa::ImportedHeaders::< @@ -190,7 +195,8 @@ pub mod pallet { storage: &bp_runtime::StorageProofChecker, parachain: ParaId, ) -> Option { - let parachain_head_key = storage_keys::parachain_head_key(parachain); + let parachain_head_key = + parachain_head_storage_key_at_source(T::ParasPalletName::get(), parachain); let parachain_head = storage.read_value(parachain_head_key.0.as_ref()).ok()??; let parachain_head = ParaHead::decode(&mut ¶chain_head[..]).ok()?; Some(parachain_head) @@ -257,6 +263,12 @@ pub mod pallet { updated_head_hash, ); ImportedParaHeads::::insert(parachain, updated_head_hash, updated_head); + log::trace!( + target: "runtime::bridge-parachains", + "Updated head of parachain {:?} to {}", + parachain, + updated_head_hash, + ); // remove old head if let Ok(head_hash_to_prune) = head_hash_to_prune { @@ -274,22 +286,10 @@ pub mod pallet { } } -pub mod storage_keys { - use super::*; - use bp_runtime::storage_map_final_key; - use frame_support::Twox64Concat; - use sp_core::storage::StorageKey; - - /// Storage key of the parachain head in the runtime storage of relay chain. - pub fn parachain_head_key(parachain: ParaId) -> StorageKey { - storage_map_final_key::("Paras", "Heads", ¶chain.encode()) - } -} - #[cfg(test)] mod tests { use super::*; - use crate::mock::{run_test, test_relay_header, Origin, TestRuntime}; + use crate::mock::{run_test, test_relay_header, Origin, TestRuntime, PARAS_PALLET_NAME}; use bp_test_utils::{authority_list, make_default_justification}; use frame_support::{assert_noop, assert_ok, traits::OnInitialize}; @@ -330,13 +330,14 @@ mod tests { fn prepare_parachain_heads_proof( heads: Vec<(ParaId, ParaHead)>, - ) -> (RelayBlockHash, ParachainHeadsProof) { + ) -> (RelayBlockHash, ParaHeadsProof) { let mut root = Default::default(); let mut mdb = MemoryDB::default(); { let mut trie = TrieDBMutV1::::new(&mut mdb, &mut root); for (parachain, head) in heads { - let storage_key = storage_keys::parachain_head_key(parachain); + let storage_key = + parachain_head_storage_key_at_source(PARAS_PALLET_NAME, parachain); trie.insert(&storage_key.0, &head.encode()) .map_err(|_| "TrieMut::insert has failed") .expect("TrieMut::insert should not fail in tests"); @@ -372,7 +373,7 @@ mod tests { fn import_parachain_1_head( relay_chain_block: RelayBlockNumber, relay_state_root: RelayBlockHash, - proof: ParachainHeadsProof, + proof: ParaHeadsProof, ) -> sp_runtime::DispatchResult { Pallet::::submit_parachain_heads( Origin::signed(1), diff --git a/bridges/modules/parachains/src/mock.rs b/bridges/modules/parachains/src/mock.rs index fcd157020bc1..506783cdb496 100644 --- a/bridges/modules/parachains/src/mock.rs +++ b/bridges/modules/parachains/src/mock.rs @@ -33,6 +33,8 @@ pub type RelayBlockHeader = type Block = frame_system::mocking::MockBlock; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +pub const PARAS_PALLET_NAME: &str = "Paras"; + construct_runtime! { pub enum TestRuntime where Block = Block, @@ -103,10 +105,12 @@ impl pallet_bridge_grandpa::Config for TestRun parameter_types! { pub const HeadsToKeep: u32 = 4; + pub const ParasPalletName: &'static str = PARAS_PALLET_NAME; } impl pallet_bridge_parachains::Config for TestRuntime { type BridgesGrandpaPalletInstance = pallet_bridge_grandpa::Instance1; + type ParasPalletName = ParasPalletName; type HeadsToKeep = HeadsToKeep; } diff --git a/bridges/primitives/chain-millau/src/lib.rs b/bridges/primitives/chain-millau/src/lib.rs index 281ea471a2cf..79c0e6362811 100644 --- a/bridges/primitives/chain-millau/src/lib.rs +++ b/bridges/primitives/chain-millau/src/lib.rs @@ -269,6 +269,9 @@ pub const WITH_MILLAU_MESSAGES_PALLET_NAME: &str = "BridgeMillauMessages"; /// Name of the Rialto->Millau (actually DOT->KSM) conversion rate stored in the Millau runtime. pub const RIALTO_TO_MILLAU_CONVERSION_RATE_PARAMETER_NAME: &str = "RialtoToMillauConversionRate"; +/// Name of the With-Rialto parachains bridge pallet name in the Millau runtime. +pub const BRIDGE_PARAS_PALLET_NAME: &str = "BridgeRialtoParachains"; + /// Name of the `MillauFinalityApi::best_finalized` runtime method. pub const BEST_FINALIZED_MILLAU_HEADER_METHOD: &str = "MillauFinalityApi_best_finalized"; diff --git a/bridges/primitives/parachains/Cargo.toml b/bridges/primitives/parachains/Cargo.toml new file mode 100644 index 000000000000..a27dd03363d4 --- /dev/null +++ b/bridges/primitives/parachains/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "bp-parachains" +description = "Primitives of parachains module." +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +serde = { version = "1.0", optional = true, features = ["derive"] } + +# Bridge dependencies + +bp-polkadot-core = { path = "../polkadot-core", default-features = false } +bp-runtime = { path = "../runtime", default-features = false } + +# Substrate dependencies + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } + +[features] +default = ["std"] +std = [ + "bp-polkadot-core/std", + "bp-runtime/std", + "codec/std", + "frame-support/std", + "scale-info/std", + "serde", + "sp-core/std", +] diff --git a/bridges/primitives/parachains/src/lib.rs b/bridges/primitives/parachains/src/lib.rs new file mode 100644 index 000000000000..0ed4e11480da --- /dev/null +++ b/bridges/primitives/parachains/src/lib.rs @@ -0,0 +1,68 @@ +// 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 . + +//! Primitives of parachains module. + +#![cfg_attr(not(feature = "std"), no_std)] + +use bp_polkadot_core::{ + parachains::{ParaHash, ParaId}, + BlockNumber as RelayBlockNumber, +}; +use codec::{Decode, Encode}; +use frame_support::{Blake2_128Concat, RuntimeDebug, Twox64Concat}; +use scale_info::TypeInfo; +use sp_core::storage::StorageKey; + +/// Best known parachain head hash. +#[derive(Clone, Decode, Encode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct BestParaHeadHash { + /// Number of relay block where this head has been read. + /// + /// Parachain head is opaque to relay chain. So we can't simply decode it as a header of + /// parachains and call `block_number()` on it. Instead, we're using the fact that parachain + /// head is always built on top of previous head (because it is blockchain) and relay chain + /// always imports parachain heads in order. What it means for us is that at any given + /// **finalized** relay block `B`, head of parachain will be ancestor (or the same) of all + /// parachain heads available at descendants of `B`. + pub at_relay_block_number: RelayBlockNumber, + /// Hash of parachain head. + pub head_hash: ParaHash, +} + +/// Returns runtime storage key of given parachain head at the source chain. +/// +/// The head is stored by the `paras` pallet in the `Heads` map. +pub fn parachain_head_storage_key_at_source( + paras_pallet_name: &str, + para_id: ParaId, +) -> StorageKey { + bp_runtime::storage_map_final_key::(paras_pallet_name, "Heads", ¶_id.encode()) +} + +/// Returns runtime storage key of best known parachain head at the target chain. +/// +/// The head is stored by the `pallet-bridge-parachains` pallet in the `BestParaHeads` map. +pub fn parachain_head_storage_key_at_target( + bridge_parachains_pallet_name: &str, + para_id: ParaId, +) -> StorageKey { + bp_runtime::storage_map_final_key::( + bridge_parachains_pallet_name, + "BestParaHeads", + ¶_id.encode(), + ) +} diff --git a/bridges/primitives/polkadot-core/src/parachains.rs b/bridges/primitives/polkadot-core/src/parachains.rs index c0448a846b34..7f8b20067758 100644 --- a/bridges/primitives/polkadot-core/src/parachains.rs +++ b/bridges/primitives/polkadot-core/src/parachains.rs @@ -79,4 +79,4 @@ impl ParaHead { pub type ParaHash = crate::Hash; /// Raw storage proof of parachain heads, stored in polkadot-like chain runtime. -pub type ParachainHeadsProof = Vec>; +pub type ParaHeadsProof = Vec>; diff --git a/bridges/relays/bin-substrate/Cargo.toml b/bridges/relays/bin-substrate/Cargo.toml index 1d1166d1a9d1..6ea4bacb191c 100644 --- a/bridges/relays/bin-substrate/Cargo.toml +++ b/bridges/relays/bin-substrate/Cargo.toml @@ -27,6 +27,7 @@ bp-kusama = { path = "../../primitives/chain-kusama" } bp-messages = { path = "../../primitives/messages" } bp-millau = { path = "../../primitives/chain-millau" } bp-polkadot = { path = "../../primitives/chain-polkadot" } +bp-polkadot-core = { path = "../../primitives/polkadot-core" } bp-rialto = { path = "../../primitives/chain-rialto" } bp-rialto-parachain = { path = "../../primitives/chain-rialto-parachain" } bp-rococo = { path = "../../primitives/chain-rococo" } @@ -39,6 +40,7 @@ messages-relay = { path = "../messages" } millau-runtime = { path = "../../bin/millau/runtime" } pallet-bridge-grandpa = { path = "../../modules/grandpa" } pallet-bridge-messages = { path = "../../modules/messages" } +parachains-relay = { path = "../parachains" } relay-kusama-client = { path = "../client-kusama" } relay-millau-client = { path = "../client-millau" } relay-polkadot-client = { path = "../client-polkadot" } diff --git a/bridges/relays/bin-substrate/src/chains/mod.rs b/bridges/relays/bin-substrate/src/chains/mod.rs index 9b3416f901cf..7659fc489763 100644 --- a/bridges/relays/bin-substrate/src/chains/mod.rs +++ b/bridges/relays/bin-substrate/src/chains/mod.rs @@ -24,6 +24,7 @@ pub mod polkadot_headers_to_kusama; pub mod polkadot_messages_to_kusama; pub mod rialto_headers_to_millau; pub mod rialto_messages_to_millau; +pub mod rialto_parachains_to_millau; pub mod rococo_headers_to_wococo; pub mod rococo_messages_to_wococo; pub mod westend_headers_to_millau; diff --git a/bridges/relays/bin-substrate/src/chains/rialto_parachains_to_millau.rs b/bridges/relays/bin-substrate/src/chains/rialto_parachains_to_millau.rs new file mode 100644 index 000000000000..492f1fdf16d0 --- /dev/null +++ b/bridges/relays/bin-substrate/src/chains/rialto_parachains_to_millau.rs @@ -0,0 +1,39 @@ +// 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 . + +//! Rialto-to-Millau parachains sync entrypoint. + +use parachains_relay::ParachainsPipeline; +use relay_millau_client::Millau; +use relay_rialto_client::Rialto; +use substrate_relay_helper::parachains_target::DirectSubmitParachainHeadsCallBuilder; + +/// Rialto-to-Millau parachains sync description. +#[derive(Clone, Debug)] +pub struct RialtoToMillauParachains; + +impl ParachainsPipeline for RialtoToMillauParachains { + type SourceChain = Rialto; + type TargetChain = Millau; +} + +/// `submit_parachain_heads` call builder for Rialto-to-Millau parachains sync pipeline. +pub type RialtoToMillauParachainsSubmitParachainHeadsCallBuilder = + DirectSubmitParachainHeadsCallBuilder< + RialtoToMillauParachains, + millau_runtime::Runtime, + millau_runtime::WitRialtoParachainsInstance, + >; diff --git a/bridges/relays/bin-substrate/src/cli/mod.rs b/bridges/relays/bin-substrate/src/cli/mod.rs index fddae4ee7abb..bf2bede5d253 100644 --- a/bridges/relays/bin-substrate/src/cli/mod.rs +++ b/bridges/relays/bin-substrate/src/cli/mod.rs @@ -36,6 +36,7 @@ mod reinit_bridge; mod relay_headers; mod relay_headers_and_messages; mod relay_messages; +mod relay_parachains; mod resubmit_transactions; /// Parse relay CLI args. @@ -85,6 +86,8 @@ pub enum Command { ResubmitTransactions(resubmit_transactions::ResubmitTransactions), /// Register parachain. RegisterParachain(register_parachain::RegisterParachain), + /// + RelayParachains(relay_parachains::RelayParachains), } impl Command { @@ -118,6 +121,7 @@ impl Command { Self::EstimateFee(arg) => arg.run().await?, Self::ResubmitTransactions(arg) => arg.run().await?, Self::RegisterParachain(arg) => arg.run().await?, + Self::RelayParachains(arg) => arg.run().await?, } Ok(()) } diff --git a/bridges/relays/bin-substrate/src/cli/relay_parachains.rs b/bridges/relays/bin-substrate/src/cli/relay_parachains.rs new file mode 100644 index 000000000000..f1af5b5c9587 --- /dev/null +++ b/bridges/relays/bin-substrate/src/cli/relay_parachains.rs @@ -0,0 +1,119 @@ +// 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 . + +use bp_polkadot_core::parachains::ParaId; +use parachains_relay::{parachains_loop::ParachainSyncParams, ParachainsPipeline}; +use relay_utils::metrics::{GlobalMetrics, StandaloneMetric}; +use structopt::StructOpt; +use strum::{EnumString, EnumVariantNames, VariantNames}; +use substrate_relay_helper::{ + parachains_source::ParachainsSource, parachains_target::ParachainsTarget, TransactionParams, +}; + +use crate::cli::{ + PrometheusParams, SourceConnectionParams, TargetConnectionParams, TargetSigningParams, +}; + +/// Start parachain heads relayer process. +#[derive(StructOpt)] +pub struct RelayParachains { + /// A bridge instance to relay parachains heads for. + #[structopt(possible_values = RelayParachainsBridge::VARIANTS, case_insensitive = true)] + bridge: RelayParachainsBridge, + #[structopt(flatten)] + source: SourceConnectionParams, + #[structopt(flatten)] + target: TargetConnectionParams, + #[structopt(flatten)] + target_sign: TargetSigningParams, + #[structopt(flatten)] + prometheus_params: PrometheusParams, +} + +/// Parachain heads relay bridge. +#[derive(Debug, EnumString, EnumVariantNames)] +#[strum(serialize_all = "kebab_case")] +pub enum RelayParachainsBridge { + RialtoToMillau, +} + +macro_rules! select_bridge { + ($bridge: expr, $generic: tt) => { + match $bridge { + RelayParachainsBridge::RialtoToMillau => { + use crate::chains::rialto_parachains_to_millau::{ + RialtoToMillauParachains as Pipeline, + RialtoToMillauParachainsSubmitParachainHeadsCallBuilder as SubmitParachainHeadsCallBuilder, + }; + + use bp_millau::BRIDGE_PARAS_PALLET_NAME as BRIDGE_PARAS_PALLET_NAME_AT_TARGET; + use bp_rialto::PARAS_PALLET_NAME as PARAS_PALLET_NAME_AT_SOURCE; + + use relay_millau_client::Millau as TargetTransactionSignScheme; + + $generic + }, + } + }; +} + +impl RelayParachains { + /// Run the command. + pub async fn run(self) -> anyhow::Result<()> { + select_bridge!(self.bridge, { + type SourceChain = ::SourceChain; + type TargetChain = ::TargetChain; + + let source_client = self.source.to_client::().await?; + let source_client = ParachainsSource::::new( + source_client, + PARAS_PALLET_NAME_AT_SOURCE.into(), + ); + + let taret_transaction_params = TransactionParams { + signer: self.target_sign.to_keypair::()?, + mortality: self.target_sign.target_transactions_mortality, + }; + let target_client = self.target.to_client::().await?; + let target_client = ParachainsTarget::< + Pipeline, + TargetTransactionSignScheme, + SubmitParachainHeadsCallBuilder, + >::new( + target_client.clone(), + taret_transaction_params, + BRIDGE_PARAS_PALLET_NAME_AT_TARGET.into(), + ); + + let metrics_params: relay_utils::metrics::MetricsParams = self.prometheus_params.into(); + GlobalMetrics::new()?.register_and_spawn(&metrics_params.registry)?; + + parachains_relay::parachains_loop::run( + source_client, + target_client, + ParachainSyncParams { + parachains: vec![ParaId(2000)], + stall_timeout: std::time::Duration::from_secs(60), + strategy: parachains_relay::parachains_loop::ParachainSyncStrategy::Any, + }, + metrics_params, + futures::future::pending(), + ) + .await + .map_err(|e| anyhow::format_err!("{}", e)) + }) + } +} diff --git a/bridges/relays/client-substrate/Cargo.toml b/bridges/relays/client-substrate/Cargo.toml index 5298009d8718..658755958be8 100644 --- a/bridges/relays/client-substrate/Cargo.toml +++ b/bridges/relays/client-substrate/Cargo.toml @@ -9,6 +9,7 @@ license = "GPL-3.0-or-later WITH Classpath-exception-2.0" async-std = { version = "1.6.5", features = ["attributes"] } async-trait = "0.1" codec = { package = "parity-scale-codec", version = "3.0.0" } +futures = "0.3.7" jsonrpsee = { version = "0.8", features = ["macros", "ws-client"] } log = "0.4.11" num-traits = "0.2" @@ -44,5 +45,6 @@ sp-storage = { git = "https://github.com/paritytech/substrate", branch = "master sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-version = { git = "https://github.com/paritytech/substrate", branch = "master" } -#[dev-dependencies] -futures = "0.3.7" +[features] +default = [] +test-helpers = [] diff --git a/bridges/relays/client-substrate/src/guard.rs b/bridges/relays/client-substrate/src/guard.rs index 359a3f69d8a3..bc8fd8e7d302 100644 --- a/bridges/relays/client-substrate/src/guard.rs +++ b/bridges/relays/client-substrate/src/guard.rs @@ -196,7 +196,7 @@ impl Environment for Client { #[cfg(test)] mod tests { use super::*; - use frame_support::weights::{IdentityFee, Weight}; + use crate::test_chain::TestChain; use futures::{ channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, future::FutureExt, @@ -204,49 +204,6 @@ mod tests { SinkExt, }; - #[derive(Debug, Clone)] - struct TestChain; - - impl bp_runtime::Chain for TestChain { - type BlockNumber = u32; - type Hash = sp_core::H256; - type Hasher = sp_runtime::traits::BlakeTwo256; - type Header = sp_runtime::generic::Header; - - type AccountId = u32; - type Balance = u32; - type Index = u32; - type Signature = sp_runtime::testing::TestSignature; - - fn max_extrinsic_size() -> u32 { - unreachable!() - } - fn max_extrinsic_weight() -> Weight { - unreachable!() - } - } - - impl Chain for TestChain { - const NAME: &'static str = "Test"; - const TOKEN_ID: Option<&'static str> = None; - const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "BestTestHeader"; - const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(1); - const STORAGE_PROOF_OVERHEAD: u32 = 0; - const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 0; - - type SignedBlock = sp_runtime::generic::SignedBlock< - sp_runtime::generic::Block, - >; - type Call = (); - type WeightToFee = IdentityFee; - } - - impl ChainWithBalances for TestChain { - fn account_info_storage_key(_account_id: &u32) -> sp_core::storage::StorageKey { - unreachable!() - } - } - struct TestEnvironment { runtime_version_rx: UnboundedReceiver, free_native_balance_rx: UnboundedReceiver, diff --git a/bridges/relays/client-substrate/src/lib.rs b/bridges/relays/client-substrate/src/lib.rs index b3a7ec414190..d6cab3dd91bb 100644 --- a/bridges/relays/client-substrate/src/lib.rs +++ b/bridges/relays/client-substrate/src/lib.rs @@ -26,6 +26,7 @@ mod sync_header; pub mod guard; pub mod metrics; +pub mod test_chain; use std::time::Duration; diff --git a/bridges/relays/client-substrate/src/test_chain.rs b/bridges/relays/client-substrate/src/test_chain.rs new file mode 100644 index 000000000000..c7c9983efe9b --- /dev/null +++ b/bridges/relays/client-substrate/src/test_chain.rs @@ -0,0 +1,71 @@ +// 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 . + +//! Pallet provides a set of guard functions that are running in background threads +//! and are aborting process if some condition fails. + +//! Test chain implementation to use in tests. + +#![cfg(any(feature = "test-helpers", test))] + +use crate::{Chain, ChainWithBalances}; +use frame_support::weights::{IdentityFee, Weight}; +use std::time::Duration; + +/// Chain that may be used in tests. +#[derive(Clone, Debug, PartialEq)] +pub struct TestChain; + +impl bp_runtime::Chain for TestChain { + type BlockNumber = u32; + type Hash = sp_core::H256; + type Hasher = sp_runtime::traits::BlakeTwo256; + type Header = sp_runtime::generic::Header; + + type AccountId = u32; + type Balance = u32; + type Index = u32; + type Signature = sp_runtime::testing::TestSignature; + + fn max_extrinsic_size() -> u32 { + unreachable!() + } + + fn max_extrinsic_weight() -> Weight { + unreachable!() + } +} + +impl Chain for TestChain { + const NAME: &'static str = "Test"; + const TOKEN_ID: Option<&'static str> = None; + const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestMethod"; + const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0); + const STORAGE_PROOF_OVERHEAD: u32 = 0; + const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 0; + + type SignedBlock = sp_runtime::generic::SignedBlock< + sp_runtime::generic::Block, + >; + type Call = (); + type WeightToFee = IdentityFee; +} + +impl ChainWithBalances for TestChain { + fn account_info_storage_key(_account_id: &u32) -> sp_core::storage::StorageKey { + unreachable!() + } +} diff --git a/bridges/relays/lib-substrate-relay/Cargo.toml b/bridges/relays/lib-substrate-relay/Cargo.toml index b8bfcc9601dd..a1370744b144 100644 --- a/bridges/relays/lib-substrate-relay/Cargo.toml +++ b/bridges/relays/lib-substrate-relay/Cargo.toml @@ -18,16 +18,20 @@ log = "0.4.14" # Bridge dependencies bp-header-chain = { path = "../../primitives/header-chain" } +bp-parachains = { path = "../../primitives/parachains" } +bp-polkadot-core = { path = "../../primitives/polkadot-core" } bridge-runtime-common = { path = "../../bin/runtime-common" } finality-grandpa = { version = "0.15.0" } finality-relay = { path = "../finality" } +parachains-relay = { path = "../parachains" } relay-utils = { path = "../utils" } messages-relay = { path = "../messages" } relay-substrate-client = { path = "../client-substrate" } pallet-bridge-grandpa = { path = "../../modules/grandpa" } pallet-bridge-messages = { path = "../../modules/messages" } +pallet-bridge-parachains = { path = "../../modules/parachains" } bp-runtime = { path = "../../primitives/runtime" } bp-messages = { path = "../../primitives/messages" } diff --git a/bridges/relays/lib-substrate-relay/src/lib.rs b/bridges/relays/lib-substrate-relay/src/lib.rs index bd51925f5273..374ab915166a 100644 --- a/bridges/relays/lib-substrate-relay/src/lib.rs +++ b/bridges/relays/lib-substrate-relay/src/lib.rs @@ -29,6 +29,8 @@ pub mod messages_metrics; pub mod messages_source; pub mod messages_target; pub mod on_demand_headers; +pub mod parachains_source; +pub mod parachains_target; /// Default relay loop stall timeout. If transactions generated by relay are immortal, then /// this timeout is used. diff --git a/bridges/relays/lib-substrate-relay/src/parachains_source.rs b/bridges/relays/lib-substrate-relay/src/parachains_source.rs new file mode 100644 index 000000000000..9e8d378d8bb8 --- /dev/null +++ b/bridges/relays/lib-substrate-relay/src/parachains_source.rs @@ -0,0 +1,91 @@ +// 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 . + +//! Parachain heads source. + +use async_trait::async_trait; +use bp_parachains::parachain_head_storage_key_at_source; +use bp_polkadot_core::parachains::{ParaHash, ParaHead, ParaHeadsProof, ParaId}; +use codec::Decode; +use parachains_relay::{parachains_loop::SourceClient, ParachainsPipeline}; +use relay_substrate_client::{Client, Error as SubstrateError, HeaderIdOf}; +use relay_utils::relay_loop::Client as RelayClient; + +/// Substrate client as parachain heads source. +#[derive(Clone)] +pub struct ParachainsSource { + client: Client, + paras_pallet_name: String, +} + +impl ParachainsSource

{ + /// Creates new parachains source client. + pub fn new(client: Client, paras_pallet_name: String) -> Self { + ParachainsSource { client, paras_pallet_name } + } +} + +#[async_trait] +impl RelayClient for ParachainsSource

{ + type Error = SubstrateError; + + async fn reconnect(&mut self) -> Result<(), SubstrateError> { + self.client.reconnect().await + } +} + +#[async_trait] +impl SourceClient

for ParachainsSource

{ + async fn ensure_synced(&self) -> Result { + match self.client.ensure_synced().await { + Ok(_) => Ok(true), + Err(SubstrateError::ClientNotSynced(_)) => Ok(false), + Err(e) => Err(e), + } + } + + async fn parachain_head( + &self, + at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, Self::Error> { + let storage_key = parachain_head_storage_key_at_source(&self.paras_pallet_name, para_id); + let para_head = self.client.raw_storage_value(storage_key, Some(at_block.1)).await?; + let para_head = para_head.map(|h| ParaHead::decode(&mut &h.0[..])).transpose()?; + let para_hash = para_head.map(|h| h.hash()); + + Ok(para_hash) + } + + async fn prove_parachain_heads( + &self, + at_block: HeaderIdOf, + parachains: &[ParaId], + ) -> Result { + let storage_keys = parachains + .iter() + .map(|para_id| parachain_head_storage_key_at_source(&self.paras_pallet_name, *para_id)) + .collect(); + let parachain_heads_proof = self + .client + .prove_storage(storage_keys, at_block.1) + .await? + .iter_nodes() + .collect(); + + Ok(parachain_heads_proof) + } +} diff --git a/bridges/relays/lib-substrate-relay/src/parachains_target.rs b/bridges/relays/lib-substrate-relay/src/parachains_target.rs new file mode 100644 index 000000000000..ed0456c06f21 --- /dev/null +++ b/bridges/relays/lib-substrate-relay/src/parachains_target.rs @@ -0,0 +1,211 @@ +// 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 . + +//! Parachain heads target. + +use crate::TransactionParams; + +use async_trait::async_trait; +use bp_parachains::{parachain_head_storage_key_at_target, BestParaHeadHash}; +use bp_polkadot_core::parachains::{ParaHeadsProof, ParaId}; +use codec::{Decode, Encode}; +use pallet_bridge_parachains::{ + Call as BridgeParachainsCall, Config as BridgeParachainsConfig, RelayBlockHash, + RelayBlockHasher, RelayBlockNumber, +}; +use parachains_relay::{parachains_loop::TargetClient, ParachainsPipeline}; +use relay_substrate_client::{ + AccountIdOf, AccountKeyPairOf, BlockNumberOf, CallOf, Chain, Client, Error as SubstrateError, + HashOf, HeaderIdOf, SignParam, TransactionEra, TransactionSignScheme, UnsignedTransaction, +}; +use relay_utils::{relay_loop::Client as RelayClient, HeaderId}; +use sp_core::{Bytes, Pair}; +use sp_runtime::traits::Header as HeaderT; +use std::marker::PhantomData; + +/// Different ways of building `submit_parachain_heads` calls. +pub trait SubmitParachainHeadsCallBuilder: 'static + Send + Sync { + /// Given parachains and their heads proof, build call of `submit_parachain_heads` + /// function of bridge parachains module at the target chain. + fn build_submit_parachain_heads_call( + relay_block_hash: HashOf, + parachains: Vec, + parachain_heads_proof: ParaHeadsProof, + ) -> CallOf; +} + +/// Building `submit_parachain_heads` call when you have direct access to the target +/// chain runtime. +pub struct DirectSubmitParachainHeadsCallBuilder { + _phantom: PhantomData<(P, R, I)>, +} + +impl SubmitParachainHeadsCallBuilder

for DirectSubmitParachainHeadsCallBuilder +where + P: ParachainsPipeline, + P::SourceChain: Chain, + R: BridgeParachainsConfig + Send + Sync, + I: 'static + Send + Sync, + R::BridgedChain: bp_runtime::Chain< + BlockNumber = RelayBlockNumber, + Hash = RelayBlockHash, + Hasher = RelayBlockHasher, + >, + CallOf: From>, +{ + fn build_submit_parachain_heads_call( + relay_block_hash: HashOf, + parachains: Vec, + parachain_heads_proof: ParaHeadsProof, + ) -> CallOf { + BridgeParachainsCall::::submit_parachain_heads { + relay_block_hash, + parachains, + parachain_heads_proof, + } + .into() + } +} + +/// Substrate client as parachain heads source. +pub struct ParachainsTarget { + client: Client, + transaction_params: TransactionParams>, + bridge_paras_pallet_name: String, + _phantom: PhantomData, +} + +impl ParachainsTarget { + /// Creates new parachains target client. + pub fn new( + client: Client, + transaction_params: TransactionParams>, + bridge_paras_pallet_name: String, + ) -> Self { + ParachainsTarget { + client, + transaction_params, + bridge_paras_pallet_name, + _phantom: Default::default(), + } + } +} + +impl Clone for ParachainsTarget { + fn clone(&self) -> Self { + ParachainsTarget { + client: self.client.clone(), + transaction_params: self.transaction_params.clone(), + bridge_paras_pallet_name: self.bridge_paras_pallet_name.clone(), + _phantom: Default::default(), + } + } +} + +#[async_trait] +impl< + P: ParachainsPipeline, + S: 'static + TransactionSignScheme, + CB: SubmitParachainHeadsCallBuilder

, + > RelayClient for ParachainsTarget +{ + type Error = SubstrateError; + + async fn reconnect(&mut self) -> Result<(), SubstrateError> { + self.client.reconnect().await + } +} + +#[async_trait] +impl TargetClient

for ParachainsTarget +where + P: ParachainsPipeline, + S: 'static + TransactionSignScheme, + CB: SubmitParachainHeadsCallBuilder

, + AccountIdOf: From< as Pair>::Public>, +{ + async fn best_block(&self) -> Result, Self::Error> { + let best_header = self.client.best_header().await?; + let best_hash = best_header.hash(); + let best_id = HeaderId(*best_header.number(), best_hash); + + Ok(best_id) + } + + async fn best_finalized_source_block( + &self, + at_block: &HeaderIdOf, + ) -> Result, Self::Error> { + let encoded_best_finalized_source_block = self + .client + .state_call( + P::SourceChain::BEST_FINALIZED_HEADER_ID_METHOD.into(), + Bytes(Vec::new()), + Some(at_block.1), + ) + .await?; + let decoded_best_finalized_source_block: ( + BlockNumberOf, + HashOf, + ) = Decode::decode(&mut &encoded_best_finalized_source_block.0[..]) + .map_err(SubstrateError::ResponseParseFailed)?; + Ok(HeaderId(decoded_best_finalized_source_block.0, decoded_best_finalized_source_block.1)) + } + + async fn parachain_head( + &self, + at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, Self::Error> { + let storage_key = + parachain_head_storage_key_at_target(&self.bridge_paras_pallet_name, para_id); + let para_head = self.client.storage_value(storage_key, Some(at_block.1)).await?; + + Ok(para_head) + } + + async fn submit_parachain_heads_proof( + &self, + at_relay_block: HeaderIdOf, + updated_parachains: Vec, + proof: ParaHeadsProof, + ) -> Result<(), Self::Error> { + let genesis_hash = *self.client.genesis_hash(); + let transaction_params = self.transaction_params.clone(); + let (spec_version, transaction_version) = self.client.simple_runtime_version().await?; + let call = + CB::build_submit_parachain_heads_call(at_relay_block.1, updated_parachains, proof); + self.client + .submit_signed_extrinsic( + self.transaction_params.signer.public().into(), + move |best_block_id, transaction_nonce| { + Ok(Bytes( + S::sign_transaction(SignParam { + spec_version, + transaction_version, + genesis_hash, + signer: transaction_params.signer, + era: TransactionEra::new(best_block_id, transaction_params.mortality), + unsigned: UnsignedTransaction::new(call.into(), transaction_nonce), + })? + .encode(), + )) + }, + ) + .await + .map(drop) + } +} diff --git a/bridges/relays/parachains/Cargo.toml b/bridges/relays/parachains/Cargo.toml new file mode 100644 index 000000000000..e5c0cea137bf --- /dev/null +++ b/bridges/relays/parachains/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "parachains-relay" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +async-std = "1.6.5" +async-trait = "0.1.40" +backoff = "0.2" +futures = "0.3.5" +linked-hash-map = "0.5.3" +log = "0.4.11" +num-traits = "0.2" +parking_lot = "0.11.0" +relay-utils = { path = "../utils" } + +# Bridge dependencies + +bp-parachains = { path = "../../primitives/parachains" } +bp-polkadot-core = { path = "../../primitives/polkadot-core" } +relay-substrate-client = { path = "../client-substrate" } + +[dev-dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0" } +relay-substrate-client = { path = "../client-substrate", features = ["test-helpers"] } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" } diff --git a/bridges/relays/parachains/src/lib.rs b/bridges/relays/parachains/src/lib.rs new file mode 100644 index 000000000000..94b3ce3ec766 --- /dev/null +++ b/bridges/relays/parachains/src/lib.rs @@ -0,0 +1,30 @@ +// 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 . + +use std::fmt::Debug; + +use relay_substrate_client::Chain; + +pub mod parachains_loop; +pub mod parachains_loop_metrics; + +/// Finality proofs synchronization pipeline. +pub trait ParachainsPipeline: 'static + Clone + Debug + Send + Sync { + /// Relay chain which is storing parachain heads in its `paras` module. + type SourceChain: Chain; + /// Target chain (either relay or para) which wants to know about new parachain heads. + type TargetChain: Chain; +} diff --git a/bridges/relays/parachains/src/parachains_loop.rs b/bridges/relays/parachains/src/parachains_loop.rs new file mode 100644 index 000000000000..56ceb67c58d2 --- /dev/null +++ b/bridges/relays/parachains/src/parachains_loop.rs @@ -0,0 +1,1020 @@ +// 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 . + +use crate::{parachains_loop_metrics::ParachainsLoopMetrics, ParachainsPipeline}; + +use async_trait::async_trait; +use bp_parachains::BestParaHeadHash; +use bp_polkadot_core::{ + parachains::{ParaHash, ParaHeadsProof, ParaId}, + BlockNumber as RelayBlockNumber, +}; +use futures::{future::FutureExt, select}; +use relay_substrate_client::{BlockNumberOf, Chain, HeaderIdOf}; +use relay_utils::{metrics::MetricsParams, relay_loop::Client as RelayClient, FailedClient}; +use std::{ + collections::{BTreeMap, BTreeSet}, + future::Future, + time::{Duration, Instant}, +}; + +/// Parachain heads synchronization params. +#[derive(Clone, Debug)] +pub struct ParachainSyncParams { + /// Parachains that we're relaying here. + pub parachains: Vec, + /// Parachain heads update strategy. + pub strategy: ParachainSyncStrategy, + /// Stall timeout. If we have submitted transaction and we see no state updates for this + /// period, we consider our transaction lost. + pub stall_timeout: Duration, +} + +/// Parachain heads update strategy. +#[derive(Clone, Copy, Debug)] +pub enum ParachainSyncStrategy { + /// Update whenever any parachain head is updated. + Any, + /// Wait till all parachain heads are updated. + All, +} + +/// Source client used in parachain heads synchronization loop. +#[async_trait] +pub trait SourceClient: RelayClient { + /// Returns `Ok(true)` if client is in synced state. + async fn ensure_synced(&self) -> Result; + + /// Get parachain head hash at given block. + async fn parachain_head( + &self, + at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, Self::Error>; + + /// Get parachain heads proof. + async fn prove_parachain_heads( + &self, + at_block: HeaderIdOf, + parachains: &[ParaId], + ) -> Result; +} + +/// Target client used in parachain heads synchronization loop. +#[async_trait] +pub trait TargetClient: RelayClient { + /// Get best block id. + async fn best_block(&self) -> Result, Self::Error>; + + /// Get best finalized source block id. + async fn best_finalized_source_block( + &self, + at_block: &HeaderIdOf, + ) -> Result, Self::Error>; + + /// Get parachain head hash at given block. + async fn parachain_head( + &self, + at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, Self::Error>; + + /// Submit parachain heads proof. + async fn submit_parachain_heads_proof( + &self, + at_source_block: HeaderIdOf, + updated_parachains: Vec, + proof: ParaHeadsProof, + ) -> Result<(), Self::Error>; +} + +/// Return prefix that will be used by default to expose Prometheus metrics of the parachains +/// sync loop. +pub fn metrics_prefix() -> String { + format!("{}_to_{}_Sync", P::SourceChain::NAME, P::TargetChain::NAME) +} + +/// Run parachain heads synchronization. +pub async fn run( + source_client: impl SourceClient

, + target_client: impl TargetClient

, + sync_params: ParachainSyncParams, + metrics_params: MetricsParams, + exit_signal: impl Future + 'static + Send, +) -> Result<(), relay_utils::Error> +where + P::SourceChain: Chain, +{ + let exit_signal = exit_signal.shared(); + relay_utils::relay_loop(source_client, target_client) + .with_metrics(metrics_params) + .loop_metric(ParachainsLoopMetrics::new(Some(&metrics_prefix::

()))?)? + .expose() + .await? + .run(metrics_prefix::

(), move |source_client, target_client, metrics| { + run_until_connection_lost( + source_client, + target_client, + sync_params.clone(), + metrics, + exit_signal.clone(), + ) + }) + .await +} + +/// Run parachain heads synchronization. +async fn run_until_connection_lost( + source_client: impl SourceClient

, + target_client: impl TargetClient

, + sync_params: ParachainSyncParams, + _metrics: Option, + exit_signal: impl Future + Send, +) -> Result<(), FailedClient> +where + P::SourceChain: Chain, +{ + let exit_signal = exit_signal.fuse(); + let min_block_interval = std::cmp::min( + P::SourceChain::AVERAGE_BLOCK_INTERVAL, + P::TargetChain::AVERAGE_BLOCK_INTERVAL, + ); + + let mut tx_tracker: Option> = None; + + futures::pin_mut!(exit_signal); + + // Note that the internal loop breaks with `FailedClient` error even if error is non-connection. + // It is Ok for now, but it may need to be fixed in the future to use exponential backoff for + // regular errors. + + loop { + // either wait for new block, or exit signal + select! { + _ = async_std::task::sleep(min_block_interval).fuse() => {}, + _ = exit_signal => return Ok(()), + } + + // if source client is not yet synced, we'll need to sleep. Otherwise we risk submitting too + // much redundant transactions + match source_client.ensure_synced().await { + Ok(true) => (), + Ok(false) => { + log::warn!( + target: "bridge", + "{} client is syncing. Won't do anything until it is synced", + P::SourceChain::NAME, + ); + continue + }, + Err(e) => { + log::warn!( + target: "bridge", + "{} client has failed to return its sync status: {:?}", + P::SourceChain::NAME, + e, + ); + return Err(FailedClient::Target) + }, + } + + // if we have active transaction, we'll need to wait until it is mined or dropped + let best_target_block = target_client.best_block().await.map_err(|e| { + log::warn!(target: "bridge", "Failed to read best {} block: {:?}", P::SourceChain::NAME, e); + FailedClient::Target + })?; + let heads_at_target = + read_heads_at_target(&target_client, &best_target_block, &sync_params.parachains) + .await?; + tx_tracker = tx_tracker.take().and_then(|tx_tracker| tx_tracker.update(&heads_at_target)); + if tx_tracker.is_some() { + continue + } + + // we have no active transaction and may need to update heads, but do we have something for + // update? + let best_finalized_relay_block = target_client + .best_finalized_source_block(&best_target_block) + .await + .map_err(|e| { + log::warn!( + target: "bridge", + "Failed to read best finalized {} block from {}: {:?}", + P::SourceChain::NAME, + P::TargetChain::NAME, + e, + ); + FailedClient::Target + })?; + let heads_at_source = read_heads_at_source( + &source_client, + &best_finalized_relay_block, + &sync_params.parachains, + ) + .await?; + let updated_ids = select_parachains_to_update::

( + heads_at_source, + heads_at_target, + best_finalized_relay_block, + ); + let is_update_required = is_update_required(&sync_params, &updated_ids); + + log::info!( + target: "bridge", + "Total {} parachains: {}. Up-to-date at {}: {}. Needs update at {}: {}.", + P::SourceChain::NAME, + sync_params.parachains.len(), + P::TargetChain::NAME, + sync_params.parachains.len() - updated_ids.len(), + P::TargetChain::NAME, + updated_ids.len(), + ); + + if is_update_required { + let heads_proofs = source_client + .prove_parachain_heads(best_finalized_relay_block, &updated_ids) + .await + .map_err(|e| { + log::warn!( + target: "bridge", + "Failed to prove {} parachain heads: {:?}", + P::SourceChain::NAME, + e, + ); + FailedClient::Source + })?; + log::info!( + target: "bridge", + "Submitting {} parachain heads update transaction to {}", + P::SourceChain::NAME, + P::TargetChain::NAME, + ); + target_client + .submit_parachain_heads_proof( + best_finalized_relay_block, + updated_ids.clone(), + heads_proofs, + ) + .await + .map_err(|e| { + log::warn!( + target: "bridge", + "Failed to submit {} parachain heads proof to {}: {:?}", + P::SourceChain::NAME, + P::TargetChain::NAME, + e, + ); + FailedClient::Target + })?; + + tx_tracker = Some(TransactionTracker::

::new( + updated_ids, + best_finalized_relay_block.0, + sync_params.stall_timeout, + )); + } + } +} + +/// Given heads at source and target clients, returns set of heads that are out of sync. +fn select_parachains_to_update( + heads_at_source: BTreeMap>, + heads_at_target: BTreeMap>, + best_finalized_relay_block: HeaderIdOf, +) -> Vec +where + P::SourceChain: Chain, +{ + heads_at_source + .into_iter() + .zip(heads_at_target.into_iter()) + .filter(|((para, head_at_source), (_, head_at_target))| { + let needs_update = match (head_at_source, head_at_target) { + (Some(head_at_source), Some(head_at_target)) + if head_at_target.at_relay_block_number < best_finalized_relay_block.0 && + head_at_target.head_hash != *head_at_source => + { + // source client knows head that is better than the head known to the target + // client + true + }, + (Some(_), Some(_)) => { + // this is normal case when relay has recently updated heads, when parachain is + // not progressing or when our source client is + false + }, + (Some(_), None) => { + // parachain is not yet known to the target client. This is true when parachain + // or bridge has been just onboarded/started + true + }, + (None, Some(_)) => { + // parachain/parathread has been offboarded removed from the system. It needs to + // be propageted to the target client + true + }, + (None, None) => { + // all's good - parachain is unknown to both clients + false + }, + }; + if needs_update { + log::trace!( + target: "bridge", + "{} parachain {:?} needs update at {}: {:?} vs {:?}", + P::SourceChain::NAME, + para, + P::TargetChain::NAME, + head_at_source, + head_at_target, + ); + } + + needs_update + }) + .map(|((para_id, _), _)| para_id) + .collect() +} + +/// Returns true if we need to submit update transactions to the target node. +fn is_update_required(sync_params: &ParachainSyncParams, updated_ids: &[ParaId]) -> bool { + match sync_params.strategy { + ParachainSyncStrategy::All => updated_ids.len() == sync_params.parachains.len(), + ParachainSyncStrategy::Any => !updated_ids.is_empty(), + } +} + +/// Reads given parachains heads from the source client. +/// +/// Guarantees that the returning map will have an entry for every parachain from `parachains`. +async fn read_heads_at_source( + source_client: &impl SourceClient

, + at_relay_block: &HeaderIdOf, + parachains: &[ParaId], +) -> Result>, FailedClient> { + let mut para_head_hashes = BTreeMap::new(); + for para in parachains { + let para_head = source_client.parachain_head(*at_relay_block, *para).await; + match para_head { + Ok(para_head) => { + para_head_hashes.insert(*para, para_head); + }, + Err(e) => { + log::warn!( + target: "bridge", + "Failed to read head of {} parachain {:?}: {:?}", + P::SourceChain::NAME, + para, + e, + ); + return Err(FailedClient::Source) + }, + } + } + Ok(para_head_hashes) +} + +/// Reads given parachains heads from the source client. +/// +/// Guarantees that the returning map will have an entry for every parachain from `parachains`. +async fn read_heads_at_target( + target_client: &impl TargetClient

, + at_block: &HeaderIdOf, + parachains: &[ParaId], +) -> Result>, FailedClient> { + let mut para_best_head_hashes = BTreeMap::new(); + for para in parachains { + let para_best_head = target_client.parachain_head(*at_block, *para).await; + match para_best_head { + Ok(para_best_head) => { + para_best_head_hashes.insert(*para, para_best_head); + }, + Err(e) => { + log::warn!( + target: "bridge", + "Failed to read head of {} parachain {:?} at {}: {:?}", + P::SourceChain::NAME, + para, + P::TargetChain::NAME, + e, + ); + return Err(FailedClient::Target) + }, + } + } + Ok(para_best_head_hashes) +} + +/// Parachain heads transaction tracker. +struct TransactionTracker { + /// Ids of parachains which heads were updated in the tracked transaction. + awaiting_update: BTreeSet, + /// Number of relay chain block that has been used to craft parachain heads proof. + relay_block_number: BlockNumberOf, + /// Transaction submit time. + submitted_at: Instant, + /// Transaction death time. + death_time: Instant, +} + +impl TransactionTracker

+where + P::SourceChain: Chain, +{ + /// Creates new parachain heads transaction tracker. + pub fn new( + awaiting_update: impl IntoIterator, + relay_block_number: BlockNumberOf, + stall_timeout: Duration, + ) -> Self { + let now = Instant::now(); + TransactionTracker { + awaiting_update: awaiting_update.into_iter().collect(), + relay_block_number, + submitted_at: now, + death_time: now + stall_timeout, + } + } + + /// Returns `None` if all parachain heads have been updated or we consider our transaction dead. + pub fn update( + mut self, + heads_at_target: &BTreeMap>, + ) -> Option { + // remove all pending heads that were synced + for (para, best_para_head) in heads_at_target { + if best_para_head + .as_ref() + .map(|best_para_head| { + best_para_head.at_relay_block_number >= self.relay_block_number + }) + .unwrap_or(false) + { + self.awaiting_update.remove(para); + + log::trace!( + target: "bridge", + "Head of parachain {:?} has been updated at {}: {:?}. Outdated parachains remaining: {}", + para, + P::TargetChain::NAME, + best_para_head, + self.awaiting_update.len(), + ); + } + } + + // if we have synced all required heads, we are done + if self.awaiting_update.is_empty() { + return None + } + + // if our transaction is dead now, we may start over again + let now = Instant::now(); + if now >= self.death_time { + log::warn!( + target: "bridge", + "Parachain heads update transaction {} has been lost: no updates for {}s", + P::TargetChain::NAME, + (now - self.submitted_at).as_secs(), + ); + + return None + } + + Some(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_std::sync::{Arc, Mutex}; + use codec::Encode; + use futures::{SinkExt, StreamExt}; + use relay_substrate_client::test_chain::TestChain; + use relay_utils::{HeaderId, MaybeConnectionError}; + use sp_core::H256; + + const PARA_ID: u32 = 0; + const PARA_0_HASH: ParaHash = H256([1u8; 32]); + const PARA_1_HASH: ParaHash = H256([2u8; 32]); + + #[derive(Clone, Debug)] + enum TestError { + Error, + MissingParachainHeadProof, + } + + impl MaybeConnectionError for TestError { + fn is_connection_error(&self) -> bool { + false + } + } + + #[derive(Clone, Debug, PartialEq)] + struct TestParachainsPipeline; + + impl ParachainsPipeline for TestParachainsPipeline { + type SourceChain = TestChain; + type TargetChain = TestChain; + } + + #[derive(Clone, Debug)] + struct TestClient { + data: Arc>, + } + + #[derive(Clone, Debug)] + struct TestClientData { + source_sync_status: Result, + source_heads: BTreeMap>, + source_proofs: BTreeMap, TestError>>, + + target_best_block: Result, TestError>, + target_best_finalized_source_block: Result, TestError>, + target_heads: BTreeMap>, + target_submit_result: Result<(), TestError>, + + exit_signal_sender: Option>>, + } + + impl TestClientData { + pub fn minimal() -> Self { + TestClientData { + source_sync_status: Ok(true), + source_heads: vec![(PARA_ID, Ok(PARA_0_HASH))].into_iter().collect(), + source_proofs: vec![(PARA_ID, Ok(PARA_0_HASH.encode()))].into_iter().collect(), + + target_best_block: Ok(HeaderId(0, Default::default())), + target_best_finalized_source_block: Ok(HeaderId(0, Default::default())), + target_heads: BTreeMap::new(), + target_submit_result: Ok(()), + + exit_signal_sender: None, + } + } + + pub fn with_exit_signal_sender( + sender: futures::channel::mpsc::UnboundedSender<()>, + ) -> Self { + let mut client = Self::minimal(); + client.exit_signal_sender = Some(Box::new(sender)); + client + } + } + + impl From for TestClient { + fn from(data: TestClientData) -> TestClient { + TestClient { data: Arc::new(Mutex::new(data)) } + } + } + + #[async_trait] + impl RelayClient for TestClient { + type Error = TestError; + + async fn reconnect(&mut self) -> Result<(), TestError> { + unimplemented!() + } + } + + #[async_trait] + impl SourceClient for TestClient { + async fn ensure_synced(&self) -> Result { + self.data.lock().await.source_sync_status.clone() + } + + async fn parachain_head( + &self, + _at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, TestError> { + self.data.lock().await.source_heads.get(¶_id.0).cloned().transpose() + } + + async fn prove_parachain_heads( + &self, + _at_block: HeaderIdOf, + parachains: &[ParaId], + ) -> Result { + let mut proofs = Vec::new(); + for para_id in parachains { + proofs.push( + self.data + .lock() + .await + .source_proofs + .get(¶_id.0) + .cloned() + .transpose()? + .ok_or(TestError::MissingParachainHeadProof)?, + ); + } + Ok(proofs) + } + } + + #[async_trait] + impl TargetClient for TestClient { + async fn best_block(&self) -> Result, TestError> { + self.data.lock().await.target_best_block.clone() + } + + async fn best_finalized_source_block( + &self, + _at_block: &HeaderIdOf, + ) -> Result, TestError> { + self.data.lock().await.target_best_finalized_source_block.clone() + } + + async fn parachain_head( + &self, + _at_block: HeaderIdOf, + para_id: ParaId, + ) -> Result, TestError> { + self.data.lock().await.target_heads.get(¶_id.0).cloned().transpose() + } + + async fn submit_parachain_heads_proof( + &self, + _at_source_block: HeaderIdOf, + _updated_parachains: Vec, + _proof: ParaHeadsProof, + ) -> Result<(), Self::Error> { + self.data.lock().await.target_submit_result.clone()?; + + if let Some(mut exit_signal_sender) = self.data.lock().await.exit_signal_sender.take() { + exit_signal_sender.send(()).await.unwrap(); + } + Ok(()) + } + } + + fn default_sync_params() -> ParachainSyncParams { + ParachainSyncParams { + parachains: vec![ParaId(PARA_ID)], + strategy: ParachainSyncStrategy::Any, + stall_timeout: Duration::from_secs(60), + } + } + + #[test] + fn when_source_client_fails_to_return_sync_state() { + let mut test_source_client = TestClientData::minimal(); + test_source_client.source_sync_status = Err(TestError::Error); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(test_source_client), + TestClient::from(TestClientData::minimal()), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Target), + ); + } + + #[test] + fn when_target_client_fails_to_return_best_block() { + let mut test_target_client = TestClientData::minimal(); + test_target_client.target_best_block = Err(TestError::Error); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(TestClientData::minimal()), + TestClient::from(test_target_client), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Target), + ); + } + + #[test] + fn when_target_client_fails_to_read_heads() { + let mut test_target_client = TestClientData::minimal(); + test_target_client.target_heads.insert(PARA_ID, Err(TestError::Error)); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(TestClientData::minimal()), + TestClient::from(test_target_client), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Target), + ); + } + + #[test] + fn when_target_client_fails_to_read_best_finalized_source_block() { + let mut test_target_client = TestClientData::minimal(); + test_target_client.target_best_finalized_source_block = Err(TestError::Error); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(TestClientData::minimal()), + TestClient::from(test_target_client), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Target), + ); + } + + #[test] + fn when_source_client_fails_to_read_heads() { + let mut test_source_client = TestClientData::minimal(); + test_source_client.source_heads.insert(PARA_ID, Err(TestError::Error)); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(test_source_client), + TestClient::from(TestClientData::minimal()), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Source), + ); + } + + #[test] + fn when_source_client_fails_to_prove_heads() { + let mut test_source_client = TestClientData::minimal(); + test_source_client.source_proofs.insert(PARA_ID, Err(TestError::Error)); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(test_source_client), + TestClient::from(TestClientData::minimal()), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Source), + ); + } + + #[test] + fn when_target_client_rejects_update_transaction() { + let mut test_target_client = TestClientData::minimal(); + test_target_client.target_submit_result = Err(TestError::Error); + + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(TestClientData::minimal()), + TestClient::from(test_target_client), + default_sync_params(), + None, + futures::future::pending(), + )), + Err(FailedClient::Target), + ); + } + + #[test] + fn minimal_working_case() { + let (exit_signal_sender, exit_signal) = futures::channel::mpsc::unbounded(); + assert_eq!( + async_std::task::block_on(run_until_connection_lost( + TestClient::from(TestClientData::minimal()), + TestClient::from(TestClientData::with_exit_signal_sender(exit_signal_sender)), + default_sync_params(), + None, + exit_signal.into_future().map(|(_, _)| ()), + )), + Ok(()), + ); + } + + const PARA_1_ID: u32 = PARA_ID + 1; + const SOURCE_BLOCK_NUMBER: u32 = 100; + + fn test_tx_tracker() -> TransactionTracker { + TransactionTracker::new( + vec![ParaId(PARA_ID), ParaId(PARA_1_ID)], + SOURCE_BLOCK_NUMBER, + Duration::from_secs(1), + ) + } + + #[test] + fn tx_tracker_update_when_nothing_is_updated() { + assert_eq!( + test_tx_tracker() + .update(&vec![].into_iter().collect()) + .map(|t| t.awaiting_update), + Some(test_tx_tracker().awaiting_update), + ); + } + + #[test] + fn tx_tracker_update_when_one_of_heads_is_updated_to_previous_value() { + assert_eq!( + test_tx_tracker() + .update( + &vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { + at_relay_block_number: SOURCE_BLOCK_NUMBER - 1, + head_hash: PARA_0_HASH, + }) + )] + .into_iter() + .collect() + ) + .map(|t| t.awaiting_update), + Some(test_tx_tracker().awaiting_update), + ); + } + + #[test] + fn tx_tracker_update_when_one_of_heads_is_updated() { + assert_eq!( + test_tx_tracker() + .update( + &vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { + at_relay_block_number: SOURCE_BLOCK_NUMBER, + head_hash: PARA_0_HASH, + }) + )] + .into_iter() + .collect() + ) + .map(|t| t.awaiting_update), + Some(vec![ParaId(PARA_1_ID)].into_iter().collect()), + ); + } + + #[test] + fn tx_tracker_update_when_all_heads_are_updated() { + assert_eq!( + test_tx_tracker() + .update( + &vec![ + ( + ParaId(PARA_ID), + Some(BestParaHeadHash { + at_relay_block_number: SOURCE_BLOCK_NUMBER, + head_hash: PARA_0_HASH, + }) + ), + ( + ParaId(PARA_1_ID), + Some(BestParaHeadHash { + at_relay_block_number: SOURCE_BLOCK_NUMBER, + head_hash: PARA_0_HASH, + }) + ), + ] + .into_iter() + .collect() + ) + .map(|t| t.awaiting_update), + None, + ); + } + + #[test] + fn tx_tracker_update_when_tx_is_stalled() { + let mut tx_tracker = test_tx_tracker(); + tx_tracker.death_time = Instant::now(); + assert_eq!( + tx_tracker.update(&vec![].into_iter().collect()).map(|t| t.awaiting_update), + None, + ); + } + + #[test] + fn parachain_is_not_updated_if_it_is_unknown_to_both_clients() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), None)].into_iter().collect(), + vec![(ParaId(PARA_ID), None)].into_iter().collect(), + HeaderId(10, Default::default()), + ), + Vec::::new(), + ); + } + + #[test] + fn parachain_is_not_updated_if_it_has_been_updated_at_better_relay_block() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), Some(PARA_0_HASH))].into_iter().collect(), + vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { at_relay_block_number: 20, head_hash: PARA_1_HASH }) + )] + .into_iter() + .collect(), + HeaderId(10, Default::default()), + ), + Vec::::new(), + ); + } + + #[test] + fn parachain_is_not_updated_if_hash_is_the_same_at_next_relay_block() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), Some(PARA_0_HASH))].into_iter().collect(), + vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { at_relay_block_number: 0, head_hash: PARA_0_HASH }) + )] + .into_iter() + .collect(), + HeaderId(10, Default::default()), + ), + Vec::::new(), + ); + } + + #[test] + fn parachain_is_updated_after_offboarding() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), None)].into_iter().collect(), + vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { + at_relay_block_number: 0, + head_hash: Default::default(), + }) + )] + .into_iter() + .collect(), + HeaderId(10, Default::default()), + ), + vec![ParaId(PARA_ID)], + ); + } + + #[test] + fn parachain_is_updated_after_onboarding() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), Some(PARA_0_HASH))].into_iter().collect(), + vec![(ParaId(PARA_ID), None)].into_iter().collect(), + HeaderId(10, Default::default()), + ), + vec![ParaId(PARA_ID)], + ); + } + + #[test] + fn parachain_is_updated_if_newer_head_is_known() { + assert_eq!( + select_parachains_to_update::( + vec![(ParaId(PARA_ID), Some(PARA_1_HASH))].into_iter().collect(), + vec![( + ParaId(PARA_ID), + Some(BestParaHeadHash { at_relay_block_number: 0, head_hash: PARA_0_HASH }) + )] + .into_iter() + .collect(), + HeaderId(10, Default::default()), + ), + vec![ParaId(PARA_ID)], + ); + } + + #[test] + fn is_update_required_works() { + let mut sync_params = ParachainSyncParams { + parachains: vec![ParaId(PARA_ID), ParaId(PARA_1_ID)], + strategy: ParachainSyncStrategy::Any, + stall_timeout: Duration::from_secs(60), + }; + + assert_eq!(is_update_required(&sync_params, &[]), false); + assert_eq!(is_update_required(&sync_params, &[ParaId(PARA_ID)]), true); + assert_eq!(is_update_required(&sync_params, &[ParaId(PARA_ID), ParaId(PARA_1_ID)]), true); + + sync_params.strategy = ParachainSyncStrategy::All; + assert_eq!(is_update_required(&sync_params, &[]), false); + assert_eq!(is_update_required(&sync_params, &[ParaId(PARA_ID)]), false); + assert_eq!(is_update_required(&sync_params, &[ParaId(PARA_ID), ParaId(PARA_1_ID)]), true); + } +} diff --git a/bridges/relays/parachains/src/parachains_loop_metrics.rs b/bridges/relays/parachains/src/parachains_loop_metrics.rs new file mode 100644 index 000000000000..a0a99f5c332a --- /dev/null +++ b/bridges/relays/parachains/src/parachains_loop_metrics.rs @@ -0,0 +1,34 @@ +// 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 . + +use relay_utils::metrics::{Metric, PrometheusError, Registry}; + +/// Parachains sync metrics. +#[derive(Clone)] +pub struct ParachainsLoopMetrics; + +impl ParachainsLoopMetrics { + /// Create and register parachains loop metrics. + pub fn new(_prefix: Option<&str>) -> Result { + Ok(ParachainsLoopMetrics) + } +} + +impl Metric for ParachainsLoopMetrics { + fn register(&self, _registry: &Registry) -> Result<(), PrometheusError> { + Ok(()) + } +}