diff --git a/bridges/relays/ethereum-client/src/client.rs b/bridges/relays/ethereum-client/src/client.rs index b5fe3ab6bf179..30a62a400e1dd 100644 --- a/bridges/relays/ethereum-client/src/client.rs +++ b/bridges/relays/ethereum-client/src/client.rs @@ -16,8 +16,8 @@ use crate::rpc::Ethereum; use crate::types::{ - Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SignedRawTx, Transaction, TransactionHash, - H256, U256, + Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SignedRawTx, SyncState, Transaction, + TransactionHash, H256, U256, }; use crate::{ConnectionParams, Error, Result}; @@ -25,6 +25,9 @@ use jsonrpsee::raw::RawClient; use jsonrpsee::transport::http::HttpTransportClient; use jsonrpsee::Client as RpcClient; +/// Number of headers missing from the Ethereum node for us to consider node not synced. +const MAJOR_SYNC_BLOCKS: u64 = 5; + /// The client used to interact with an Ethereum node through RPC. #[derive(Clone)] pub struct Client { @@ -53,6 +56,23 @@ impl Client { pub fn reconnect(&mut self) { self.client = Self::build_client(&self.params); } +} + +impl Client { + /// Returns true if client is connected to at least one peer and is in synced state. + pub async fn ensure_synced(&self) -> Result<()> { + match Ethereum::syncing(&self.client).await? { + SyncState::NotSyncing => Ok(()), + SyncState::Syncing(syncing) => { + let missing_headers = syncing.highest_block.saturating_sub(syncing.current_block); + if missing_headers > MAJOR_SYNC_BLOCKS.into() { + return Err(Error::ClientNotSynced(missing_headers)); + } + + Ok(()) + } + } + } /// Estimate gas usage for the given call. pub async fn estimate_gas(&self, call_request: CallRequest) -> Result { diff --git a/bridges/relays/ethereum-client/src/error.rs b/bridges/relays/ethereum-client/src/error.rs index b02e5fecf5842..afcc3c678254e 100644 --- a/bridges/relays/ethereum-client/src/error.rs +++ b/bridges/relays/ethereum-client/src/error.rs @@ -16,6 +16,8 @@ //! Ethereum node RPC errors. +use crate::types::U256; + use jsonrpsee::client::RequestError; use relay_utils::MaybeConnectionError; @@ -40,6 +42,9 @@ pub enum Error { InvalidSubstrateBlockNumber, /// An invalid index has been received from an Ethereum node. InvalidIncompleteIndex, + /// The client we're connected to is not synced, so we can't rely on its state. Contains + /// number of unsynced headers. + ClientNotSynced(U256), } impl From for Error { @@ -50,7 +55,11 @@ impl From for Error { impl MaybeConnectionError for Error { fn is_connection_error(&self) -> bool { - matches!(*self, Error::Request(RequestError::TransportError(_))) + matches!( + *self, + Error::Request(RequestError::TransportError(_)) + | Error::ClientNotSynced(_), + ) } } @@ -66,6 +75,9 @@ impl ToString for Error { Self::IncompleteTransaction => "Incomplete Ethereum Transaction (missing required field - raw)".to_string(), Self::InvalidSubstrateBlockNumber => "Received an invalid Substrate block from Ethereum Node".to_string(), Self::InvalidIncompleteIndex => "Received an invalid incomplete index from Ethereum Node".to_string(), + Self::ClientNotSynced(missing_headers) => { + format!("Ethereum client is not synced: syncing {} headers", missing_headers) + } } } } diff --git a/bridges/relays/ethereum-client/src/rpc.rs b/bridges/relays/ethereum-client/src/rpc.rs index 9739d3edbe280..3fa4f6ceb9cd9 100644 --- a/bridges/relays/ethereum-client/src/rpc.rs +++ b/bridges/relays/ethereum-client/src/rpc.rs @@ -22,11 +22,14 @@ #![allow(unused_variables)] use crate::types::{ - Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, Transaction, TransactionHash, H256, U256, U64, + Address, Bytes, CallRequest, Header, HeaderWithTransactions, Receipt, SyncState, Transaction, TransactionHash, + H256, U256, U64, }; jsonrpsee::rpc_api! { pub(crate) Ethereum { + #[rpc(method = "eth_syncing", positional_params)] + fn syncing() -> SyncState; #[rpc(method = "eth_estimateGas", positional_params)] fn estimate_gas(call_request: CallRequest) -> U256; #[rpc(method = "eth_blockNumber", positional_params)] diff --git a/bridges/relays/ethereum-client/src/types.rs b/bridges/relays/ethereum-client/src/types.rs index f64362ade0e88..1bb9233b82ea4 100644 --- a/bridges/relays/ethereum-client/src/types.rs +++ b/bridges/relays/ethereum-client/src/types.rs @@ -18,7 +18,7 @@ use headers_relay::sync_types::SourceHeader; -pub use web3::types::{Address, Bytes, CallRequest, H256, U128, U256, U64}; +pub use web3::types::{Address, Bytes, CallRequest, SyncState, H256, U128, U256, U64}; /// When header is just received from the Ethereum node, we check that it has /// both number and hash fields filled. diff --git a/bridges/relays/ethereum/src/ethereum_exchange.rs b/bridges/relays/ethereum/src/ethereum_exchange.rs index 19d9f44cef0e9..92ba211535129 100644 --- a/bridges/relays/ethereum/src/ethereum_exchange.rs +++ b/bridges/relays/ethereum/src/ethereum_exchange.rs @@ -248,6 +248,10 @@ impl TargetClient for SubstrateTransactionsTarget { } async fn best_finalized_header_id(&self) -> Result { + // we can't continue to relay exchange proofs if Substrate node is out of sync, because + // it may have already received (some of) proofs that we're going to relay + self.client.ensure_synced().await?; + self.client.best_ethereum_finalized_block().await } diff --git a/bridges/relays/ethereum/src/ethereum_sync_loop.rs b/bridges/relays/ethereum/src/ethereum_sync_loop.rs index b84626d230c6d..c8741c2fe18a6 100644 --- a/bridges/relays/ethereum/src/ethereum_sync_loop.rs +++ b/bridges/relays/ethereum/src/ethereum_sync_loop.rs @@ -130,6 +130,9 @@ impl RelayClient for EthereumHeadersSource { #[async_trait] impl SourceClient for EthereumHeadersSource { async fn best_block_number(&self) -> Result { + // we **CAN** continue to relay headers if Ethereum node is out of sync, because + // Substrate node may be missing headers that are already available at the Ethereum + self.client.best_block_number().await.map_err(Into::into) } @@ -204,6 +207,10 @@ impl RelayClient for SubstrateHeadersTarget { #[async_trait] impl TargetClient for SubstrateHeadersTarget { async fn best_header_id(&self) -> Result { + // we can't continue to relay headers if Substrate node is out of sync, because + // it may have already received (some of) headers that we're going to relay + self.client.ensure_synced().await?; + self.client.best_ethereum_block().await } diff --git a/bridges/relays/ethereum/src/substrate_sync_loop.rs b/bridges/relays/ethereum/src/substrate_sync_loop.rs index 298fe0e592545..9e15b9223826a 100644 --- a/bridges/relays/ethereum/src/substrate_sync_loop.rs +++ b/bridges/relays/ethereum/src/substrate_sync_loop.rs @@ -131,6 +131,10 @@ impl RelayClient for EthereumHeadersTarget { #[async_trait] impl TargetClient for EthereumHeadersTarget { async fn best_header_id(&self) -> Result { + // we can't continue to relay headers if Ethereum node is out of sync, because + // it may have already received (some of) headers that we're going to relay + self.client.ensure_synced().await?; + self.client.best_substrate_block(self.contract).await } diff --git a/bridges/relays/substrate-client/Cargo.toml b/bridges/relays/substrate-client/Cargo.toml index 0e4b43bf7f6b6..320abf280f040 100644 --- a/bridges/relays/substrate-client/Cargo.toml +++ b/bridges/relays/substrate-client/Cargo.toml @@ -26,6 +26,7 @@ relay-utils = { path = "../utils" } frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "master" } frame-system = { git = "https://github.com/paritytech/substrate.git", branch = "master" } pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "master" } +sc-rpc-api = { git = "https://github.com/paritytech/substrate.git", branch = "master" } sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "master" } sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "master" } sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "master" } diff --git a/bridges/relays/substrate-client/src/client.rs b/bridges/relays/substrate-client/src/client.rs index cfa644bc8c71e..a5ddf143f1b03 100644 --- a/bridges/relays/substrate-client/src/client.rs +++ b/bridges/relays/substrate-client/src/client.rs @@ -104,6 +104,17 @@ impl Client { } impl Client { + /// Returns true if client is connected to at least one peer and is in synced state. + pub async fn ensure_synced(&self) -> Result<()> { + let health = Substrate::::system_health(&self.client).await?; + let is_synced = !health.is_syncing && (!health.should_have_peers || health.peers > 0); + if is_synced { + Ok(()) + } else { + Err(Error::ClientNotSynced(health)) + } + } + /// Return hash of the genesis block. pub fn genesis_hash(&self) -> &C::Hash { &self.genesis_hash diff --git a/bridges/relays/substrate-client/src/error.rs b/bridges/relays/substrate-client/src/error.rs index c2930c21d217a..f0212ed501ae2 100644 --- a/bridges/relays/substrate-client/src/error.rs +++ b/bridges/relays/substrate-client/src/error.rs @@ -19,6 +19,7 @@ use jsonrpsee::client::RequestError; use jsonrpsee::transport::ws::WsNewDnsError; use relay_utils::MaybeConnectionError; +use sc_rpc_api::system::Health; /// Result type used by Substrate client. pub type Result = std::result::Result; @@ -38,6 +39,8 @@ pub enum Error { UninitializedBridgePallet, /// Account does not exist on the chain. AccountDoesNotExist, + /// The client we're connected to is not synced, so we can't rely on its state. + ClientNotSynced(Health), /// Custom logic error. Custom(String), } @@ -56,7 +59,11 @@ impl From for Error { impl MaybeConnectionError for Error { fn is_connection_error(&self) -> bool { - matches!(*self, Error::Request(RequestError::TransportError(_))) + matches!( + *self, + Error::Request(RequestError::TransportError(_)) + | Error::ClientNotSynced(_) + ) } } @@ -74,6 +81,7 @@ impl ToString for Error { Self::ResponseParseFailed(e) => e.what().to_string(), Self::UninitializedBridgePallet => "The Substrate bridge pallet has not been initialized yet.".into(), Self::AccountDoesNotExist => "Account does not exist on the chain".into(), + Self::ClientNotSynced(health) => format!("Substrate client is not synced: {}", health), Self::Custom(e) => e.clone(), } } diff --git a/bridges/relays/substrate-client/src/headers_source.rs b/bridges/relays/substrate-client/src/headers_source.rs index 040cc08e99766..b347a1c9f57f2 100644 --- a/bridges/relays/substrate-client/src/headers_source.rs +++ b/bridges/relays/substrate-client/src/headers_source.rs @@ -73,6 +73,8 @@ where P::Header: SourceHeader, { async fn best_block_number(&self) -> Result { + // we **CAN** continue to relay headers if source node is out of sync, because + // target node may be missing headers that are already available at the source Ok(*self.client.best_header().await?.number()) } diff --git a/bridges/relays/substrate-client/src/rpc.rs b/bridges/relays/substrate-client/src/rpc.rs index 9c76f593e99b8..2e832b4018174 100644 --- a/bridges/relays/substrate-client/src/rpc.rs +++ b/bridges/relays/substrate-client/src/rpc.rs @@ -25,6 +25,7 @@ use crate::chain::Chain; use bp_message_lane::{LaneId, MessageNonce}; use bp_runtime::InstanceId; +use sc_rpc_api::system::Health; use sp_core::{ storage::{StorageData, StorageKey}, Bytes, @@ -33,6 +34,8 @@ use sp_version::RuntimeVersion; jsonrpsee::rpc_api! { pub(crate) Substrate { + #[rpc(method = "system_health", positional_params)] + fn system_health() -> Health; #[rpc(method = "chain_getHeader", positional_params)] fn chain_get_header(block_hash: Option) -> C::Header; #[rpc(method = "chain_getFinalizedHead", positional_params)] diff --git a/bridges/relays/substrate/src/headers_target.rs b/bridges/relays/substrate/src/headers_target.rs index c962511270805..2b5f63a7feae3 100644 --- a/bridges/relays/substrate/src/headers_target.rs +++ b/bridges/relays/substrate/src/headers_target.rs @@ -73,6 +73,10 @@ where P: SubstrateHeadersSyncPipeline, { async fn best_header_id(&self) -> Result, SubstrateError> { + // we can't continue to relay headers if target node is out of sync, because + // it may have already received (some of) headers that we're going to relay + self.client.ensure_synced().await?; + let call = P::BEST_BLOCK_METHOD.into(); let data = Bytes(Vec::new()); diff --git a/bridges/relays/substrate/src/messages_source.rs b/bridges/relays/substrate/src/messages_source.rs index b9a8d01a7c258..375c469fde1a9 100644 --- a/bridges/relays/substrate/src/messages_source.rs +++ b/bridges/relays/substrate/src/messages_source.rs @@ -99,6 +99,10 @@ where P::TargetHeaderHash: Decode, { async fn state(&self) -> Result, SubstrateError> { + // we can't continue to deliver confirmations if source node is out of sync, because + // it may have already received confirmations that we're going to deliver + self.client.ensure_synced().await?; + read_client_state::<_, P::TargetHeaderHash, P::TargetHeaderNumber>( &self.client, P::BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE, diff --git a/bridges/relays/substrate/src/messages_target.rs b/bridges/relays/substrate/src/messages_target.rs index 0bc81d9be05a3..e8610aac9533c 100644 --- a/bridges/relays/substrate/src/messages_target.rs +++ b/bridges/relays/substrate/src/messages_target.rs @@ -95,6 +95,10 @@ where P::SourceHeaderHash: Decode, { async fn state(&self) -> Result, SubstrateError> { + // we can't continue to deliver messages if target node is out of sync, because + // it may have already received (some of) messages that we're going to deliver + self.client.ensure_synced().await?; + read_client_state::<_, P::SourceHeaderHash, P::SourceHeaderNumber>( &self.client, P::BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET,